aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_invoices.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/screens_invoices.c')
-rw-r--r--clients/screens_invoices.c814
1 files changed, 659 insertions, 155 deletions
diff --git a/clients/screens_invoices.c b/clients/screens_invoices.c
index ca59eef..46d2406 100644
--- a/clients/screens_invoices.c
+++ b/clients/screens_invoices.c
@@ -14,6 +14,7 @@
#include <unistd.h>
#include "client.h"
+#include "drafts.h"
#include "tui.h"
#include "formula.h"
#include "util.h"
@@ -219,12 +220,124 @@ static int customer_save(struct app *a, int64_t id, const char *name,
return ok;
}
-static void customer_form(struct app *a, int64_t id)
+/* Drafts for the customer register: new entities exist only as drafts
+ until Spara writes them (docs/TUI-GUIDELINES.md "Interaction model"). */
+
+static struct drafts g_drafts;
+static int g_drafts_ready;
+
+static struct drafts *kdrafts(void)
+{
+ if (!g_drafts_ready) {
+ drafts_init(&g_drafts);
+ g_drafts_ready = 1;
+ }
+ return &g_drafts;
+}
+
+struct cform_ctx {
+ struct app *a;
+ int64_t id;
+ const char *draft_id;
+ int deleted;
+ struct tui_action acts[3];
+ char name[256];
+ char address[512];
+ char postal[32];
+ char city[128];
+ char vat[64];
+ char email[256];
+ char your_ref[128];
+ char payment[24];
+ char notes[2048];
+};
+
+static char *customer_fields_json(const struct cform_ctx *c)
+{
+ 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, "name", c->name);
+ yyjson_mut_obj_add_strcpy(d, o, "address", c->address);
+ yyjson_mut_obj_add_strcpy(d, o, "postal_code", c->postal);
+ yyjson_mut_obj_add_strcpy(d, o, "city", c->city);
+ yyjson_mut_obj_add_strcpy(d, o, "vat_nr", c->vat);
+ yyjson_mut_obj_add_strcpy(d, o, "email", c->email);
+ yyjson_mut_obj_add_strcpy(d, o, "your_ref", c->your_ref);
+ yyjson_mut_obj_add_int(d, o, "payment_days",
+ strtoll(c->payment, NULL, 10));
+ yyjson_mut_obj_add_strcpy(d, o, "notes", c->notes);
+ char *s = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ return s;
+}
+
+static void customer_load_json(const char *json, struct cform_ctx *c)
+{
+ yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
+
+ if (!doc)
+ return;
+ yyjson_val *o = yyjson_doc_get_root(doc);
+ const struct {
+ const char *key;
+ char *dst;
+ size_t cap;
+ } fields[] = {
+ { "name", c->name, sizeof c->name },
+ { "address", c->address, sizeof c->address },
+ { "postal_code", c->postal, sizeof c->postal },
+ { "city", c->city, sizeof c->city },
+ { "vat_nr", c->vat, sizeof c->vat },
+ { "email", c->email, sizeof c->email },
+ { "your_ref", c->your_ref, sizeof c->your_ref },
+ { "notes", c->notes, sizeof c->notes },
+ };
+ for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++)
+ snprintf(fields[i].dst, fields[i].cap, "%s",
+ vstr(o, fields[i].key));
+ int64_t days = vint(o, "payment_days");
+ if (days > 0)
+ snprintf(c->payment, sizeof c->payment, "%lld", (long long)days);
+ yyjson_doc_free(doc);
+}
+
+#define CFORM_DRAFT_DELETE TUI_KEY_ACTION(0)
+
+static int cform_key(void *ud, int ch)
+{
+ struct cform_ctx *c = ud;
+
+ if (ch == TUI_KEY_MENU) {
+ c->acts[2].enabled =
+ drafts_have(kdrafts(), c->a->org, "customer", c->draft_id);
+ return TUI_HOOK_NONE;
+ }
+ if (ch != CFORM_DRAFT_DELETE)
+ return TUI_HOOK_NONE;
+ if (tui_confirm("Kund", "Radera utkastet?")) {
+ drafts_del(kdrafts(), c->a->org, "customer", c->draft_id);
+ c->deleted = 1;
+ return TUI_HOOK_BACK;
+ }
+ return TUI_HOOK_STAY;
+}
+
+static void customer_form(struct app *a, int64_t id, const char *draft_id)
{
- char name[256] = "", address[512] = "", postal[32] = "", city[128] = "";
- char vat[64] = "", email[256] = "", your_ref[128] = "";
- char payment[16] = "30", notes[2048] = "";
- if (id > 0) {
+ struct cform_ctx c;
+
+ memset(&c, 0, sizeof c);
+ c.a = a;
+ c.id = id;
+ c.draft_id = draft_id;
+ snprintf(c.payment, sizeof c.payment, "30");
+
+ const char *fields = drafts_get(kdrafts(), a->org, "customer", draft_id);
+ if (fields) {
+ customer_load_json(fields, &c);
+ } else if (id > 0) {
char args[64];
snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id);
char *resp =
@@ -234,43 +347,46 @@ static void customer_form(struct app *a, int64_t id)
free(resp);
return;
}
- struct {
+ const struct {
const char *key;
char *dst;
size_t cap;
- } fields[] = {
- { "name", name, sizeof name },
- { "address", address, sizeof address },
- { "postal_code", postal, sizeof postal },
- { "city", city, sizeof city },
- { "vat_nr", vat, sizeof vat },
- { "email", email, sizeof email },
- { "your_ref", your_ref, sizeof your_ref },
- { "notes", notes, sizeof notes },
+ } f[] = {
+ { "name", c.name, sizeof c.name },
+ { "address", c.address, sizeof c.address },
+ { "postal_code", c.postal, sizeof c.postal },
+ { "city", c.city, sizeof c.city },
+ { "vat_nr", c.vat, sizeof c.vat },
+ { "email", c.email, sizeof c.email },
+ { "your_ref", c.your_ref, sizeof c.your_ref },
+ { "notes", c.notes, sizeof c.notes },
};
- for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) {
- char path[64], *v;
- snprintf(path, sizeof path, "result.%s", fields[i].key);
+ for (size_t i = 0; i < sizeof f / sizeof f[0]; i++) {
+ char path[64];
+ char *v;
+ snprintf(path, sizeof path, "result.%s", f[i].key);
v = jstr_dup(resp, path);
if (v) {
- snprintf(fields[i].dst, fields[i].cap, "%s", v);
+ snprintf(f[i].dst, f[i].cap, "%s", v);
free(v);
}
}
- snprintf(payment, sizeof payment, "%lld",
+ snprintf(c.payment, sizeof c.payment, "%lld",
(long long)jint_val(resp, "result.payment_days", 30));
free(resp);
}
+
const char *labels[] = { "Namn", "Adress",
"Postnummer", "Ort",
"Momsreg.nr", "E-post",
"Er referens", "Betalningsdagar",
"Anteckningar" };
- char *vals[] = { name, address, postal, city, vat,
- email, your_ref, payment, notes };
- size_t caps[] = { sizeof name, sizeof address, sizeof postal,
- sizeof city, sizeof vat, sizeof email,
- sizeof your_ref, sizeof payment, sizeof notes };
+ char *vals[] = { c.name, c.address, c.postal, c.city,
+ c.vat, c.email, c.your_ref, c.payment,
+ c.notes };
+ size_t caps[] = { sizeof c.name, sizeof c.address, sizeof c.postal,
+ sizeof c.city, sizeof c.vat, sizeof c.email,
+ sizeof c.your_ref, sizeof c.payment, sizeof c.notes };
struct tui_form_field ff[10];
memset(ff, 0, sizeof ff);
for (int i = 0; i < 9; i++) {
@@ -282,22 +398,53 @@ static void customer_form(struct app *a, int64_t id)
ff[9].label = "";
ff[9].value = "Spara";
ff[9].kind = TUI_F_ACTION;
+
static int sel = 0;
+ c.acts[0] = (struct tui_action){ "customer.save", "Spara",
+ TUI_KEY_SUBMIT, 1, NULL };
+ c.acts[1] = (struct tui_action){ "customer.validate", "Validera",
+ TUI_KEY_REFRESH, 1, NULL };
+ c.acts[2] = (struct tui_action){ "customer.draft.delete", "Radera utkast",
+ CFORM_DRAFT_DELETE, 0, "inget utkast" };
for (;;) {
- int r = tui_form_run(id > 0 ? "Kund" : "Ny kund", ff, 10, 1, &sel);
+ char title[64];
+ snprintf(title, sizeof title, "%s%s", id > 0 ? "Kund" : "Ny kund",
+ drafts_have(kdrafts(), a->org, "customer", draft_id)
+ ? " <UTKAST>"
+ : "");
+ tui_set_actions(c.acts, 3);
+ int r = tui_form_run_hook(title, ff, 10, 1, cform_key, &c, &sel);
+ if (c.deleted)
+ break;
+ if (r >= 0) {
+ char *json = customer_fields_json(&c);
+ if (json) {
+ drafts_put(kdrafts(), a->org, "customer", draft_id, json);
+ free(json);
+ }
+ }
if (r == TUI_FORM_BACK)
- return;
+ break;
if (r == TUI_FORM_REFRESH) {
- customer_save(a, id, name, address, postal, city, vat, email,
- your_ref, payment, notes, 1);
+ customer_save(a, id, c.name, c.address, c.postal, c.city, c.vat,
+ c.email, c.your_ref, c.payment, c.notes, 1);
continue;
}
if (r == TUI_FORM_SUBMIT || r == 9) {
- if (customer_save(a, id, name, address, postal, city, vat, email,
- your_ref, payment, notes, 0))
- return;
+ if (customer_save(a, id, c.name, c.address, c.postal, c.city,
+ c.vat, c.email, c.your_ref, c.payment, c.notes,
+ 0)) {
+ drafts_del(kdrafts(), a->org, "customer", draft_id);
+ break;
+ }
+ char *json = customer_fields_json(&c);
+ if (json) {
+ drafts_put(kdrafts(), a->org, "customer", draft_id, json);
+ free(json);
+ }
}
}
+ tui_form_hint_extra(NULL);
}
static void customer_archive_toggle(struct app *a, int64_t id, const char *name,
@@ -319,59 +466,153 @@ static void customer_archive_toggle(struct app *a, int64_t id, const char *name,
free(r);
}
+struct clist {
+ struct app *a;
+ int *cur;
+ char **items;
+ char **names;
+ char **dkeys;
+ int64_t *ids;
+ unsigned char *active;
+ size_t nserver;
+ size_t nrows; /* server rows + temp drafts; the add row is at nrows */
+ struct tui_action act; /* 'd' for the current row (see clist_key) */
+};
+
+static void clist_clear(struct clist *l)
+{
+ if (l->items) {
+ for (size_t i = 0; i <= l->nrows; i++) {
+ free(l->items[i]);
+ free(l->names[i]);
+ free(l->dkeys[i]);
+ }
+ }
+ free(l->items);
+ free(l->names);
+ free(l->dkeys);
+ free(l->ids);
+ free(l->active);
+ l->items = NULL;
+ l->names = NULL;
+ l->dkeys = NULL;
+ l->ids = NULL;
+ l->active = NULL;
+ l->nserver = l->nrows = 0;
+}
+
+static int clist_is_server_id(const struct clist *l, const char *id)
+{
+ long long v = strtoll(id, NULL, 10);
+
+ if (v <= 0)
+ return 0;
+ for (size_t i = 0; i < l->nserver; i++)
+ if (l->ids[i] == v)
+ return 1;
+ return 0;
+}
+
+static void clist_collect_temp(void *ud, const char *id, const char *fields)
+{
+ struct clist *l = ud;
+ char nbuf[256], line[768];
+ const char *name = "(namnlös)";
+ yyjson_doc *doc;
+
+ if (clist_is_server_id(l, id))
+ return;
+ doc = yyjson_read(fields, strlen(fields), 0);
+ if (doc) {
+ yyjson_val *o = yyjson_doc_get_root(doc);
+ const char *n = vstr(o, "name");
+ if (*n)
+ name = n;
+ size_t i = l->nrows;
+ l->names[i] = xstrdup(name);
+ snprintf(nbuf, sizeof nbuf, "<UTKAST> %s", name);
+ snprintf(line, sizeof line, "%s", nbuf);
+ l->items[i] = xstrdup(line);
+ l->dkeys[i] = xstrdup(id);
+ l->ids[i] = 0;
+ l->active[i] = 1;
+ l->nrows++;
+ yyjson_doc_free(doc);
+ return;
+ }
+ size_t i = l->nrows;
+ l->names[i] = xstrdup(name);
+ snprintf(line, sizeof line, "<UTKAST> %s", name);
+ l->items[i] = xstrdup(line);
+ l->dkeys[i] = xstrdup(id);
+ l->ids[i] = 0;
+ l->active[i] = 1;
+ l->nrows++;
+}
+
+/* The row action behind 'd' depends on the row: a draft is deleted, a
+ saved customer archived or reactivated. The menu shows the right one. */
+static int clist_key(void *ud, int ch)
+{
+ struct clist *l = ud;
+
+ if (ch != TUI_KEY_MENU || !l->cur)
+ return TUI_HOOK_NONE;
+ int i = *l->cur;
+ struct tui_action *a = &l->act;
+ *a = (struct tui_action){ "customer.remove", "Arkivera", 'd', 1, NULL };
+ if (i < 0 || (size_t)i >= l->nrows) {
+ a->enabled = 0;
+ a->reason = "välj en kund";
+ } else if (l->dkeys[i] && l->ids[i] == 0) {
+ a->label = "Radera utkast";
+ a->id = "customer.draft.delete";
+ } else if (!l->active[i]) {
+ a->label = "Återaktivera";
+ }
+ return TUI_HOOK_NONE;
+}
+
void customers_screen(struct app *a)
{
- char **items = NULL;
- char **names = NULL;
- int64_t *ids = NULL;
- unsigned char *active = NULL;
- size_t n = 0;
+ struct clist l;
+ memset(&l, 0, sizeof l);
+ l.a = a;
+ int cur = 0;
+ l.cur = &cur;
int fetch = 1;
for (;;) {
if (g_quit) {
- for (size_t i = 0; i < n; i++) {
- free(items[i]);
- free(names[i]);
- }
- free(items);
- free(names);
- free(ids);
- free(active);
+ clist_clear(&l);
return;
}
if (fetch) {
- for (size_t i = 0; i < n; i++) {
- free(items[i]);
- free(names[i]);
- }
- free(items);
- free(names);
- free(ids);
- free(active);
- items = NULL;
- names = NULL;
- ids = NULL;
- active = NULL;
- n = 0;
+ clist_clear(&l);
fetch = 0;
char *resp = client_rpc(&a->conn, "customer.list", a->session,
a->org, "{}");
if (!resp || !client_ok(resp)) {
show_error("Kunder", resp);
free(resp);
+ clist_clear(&l);
return;
}
- n = jarr_size(resp, "result.items");
- items = xcalloc(n + 1, sizeof(char *));
- names = xcalloc(n + 1, sizeof(char *));
- ids = xcalloc(n ? n : 1, sizeof(int64_t));
- active = xcalloc(n + 1, 1);
+ size_t n = jarr_size(resp, "result.items");
+ size_t ndraft = drafts_count(kdrafts(), a->org, "customer");
+ size_t cap = n + ndraft + 2;
+ l.items = xcalloc(cap, sizeof(char *));
+ l.names = xcalloc(cap, sizeof(char *));
+ l.dkeys = xcalloc(cap, sizeof(char *));
+ l.ids = xcalloc(cap, sizeof(int64_t));
+ l.active = xcalloc(cap, 1);
+ l.nserver = n;
+ l.nrows = n;
for (size_t i = 0; i < n; i++) {
- char path[64], line[768];
+ char path[64], line[768], idbuf[32];
snprintf(path, sizeof path, "result.items.%zu.id", i);
- ids[i] = jint_val(resp, path, 0);
+ l.ids[i] = jint_val(resp, path, 0);
snprintf(path, sizeof path, "result.items.%zu.active", i);
- active[i] = (unsigned char)jbool_val(resp, path, 1);
+ l.active[i] = (unsigned char)jbool_val(resp, path, 1);
snprintf(path, sizeof path, "result.items.%zu.name", i);
char *name = jstr_dup(resp, path);
snprintf(path, sizeof path, "result.items.%zu.postal_code", i);
@@ -390,60 +631,83 @@ void customers_screen(struct app *a)
tui_pad_field(pbuf, sizeof pbuf, 30);
snprintf(vbuf, sizeof vbuf, "%s", vat ? vat : "");
tui_pad_field(vbuf, sizeof vbuf, 18);
- snprintf(line, sizeof line, "%s %s %s %3lld dagar %s", nbuf,
- pbuf, vbuf, (long long)days,
- active[i] ? "" : "(arkiverad)");
- items[i] = xstrdup(line);
- names[i] = xstrdup(name ? name : "");
+ snprintf(idbuf, sizeof idbuf, "%lld", (long long)l.ids[i]);
+ int isdraft =
+ drafts_have(kdrafts(), a->org, "customer", idbuf);
+ if (isdraft)
+ l.dkeys[i] = xstrdup(idbuf);
+ snprintf(line, sizeof line, "%s%s %s %s %3lld dagar %s",
+ isdraft ? "<UTKAST> " : "", nbuf, pbuf, vbuf,
+ (long long)days,
+ l.active[i] ? "" : "(arkiverad)");
+ l.items[i] = xstrdup(line);
+ l.names[i] = xstrdup(name ? name : "");
free(name);
free(postal);
free(city);
free(vat);
}
free(resp);
- items[n] = xstrdup("+ Ny kund (^N)");
+ drafts_foreach(kdrafts(), a->org, "customer", clist_collect_temp,
+ &l);
+ l.items[l.nrows] = xstrdup("+ Ny kund (n)");
+ l.names[l.nrows] = xstrdup("");
+ l.ids[l.nrows] = 0;
+ l.active[l.nrows] = 1;
}
int start = 0;
if (a->customer_sel) {
- for (size_t i = 0; i < n; i++)
- if (ids[i] == a->customer_sel) {
+ for (size_t i = 0; i < l.nserver; i++)
+ if (l.ids[i] == a->customer_sel) {
start = (int)i;
break;
}
}
- int cur = start;
- int sel = tui_select_list("Kunder", items, (int)n + 1, start, 1, &cur,
- 1, "arkivera/återaktivera", 0);
- if (cur >= 0 && (size_t)cur < n)
- a->customer_sel = ids[cur];
+ cur = start;
+ l.act = (struct tui_action){ "customer.remove", "Arkivera", 'd', 1,
+ NULL };
+ tui_set_actions(&l.act, 1);
+ int sel = tui_select_list_hook("Kunder", l.items,
+ (int)l.nrows + 1, start, 1, &cur, 1,
+ "arkivera/återaktivera", 0, clist_key,
+ &l);
+ if (cur >= 0 && (size_t)cur < l.nserver)
+ a->customer_sel = l.ids[cur];
if (sel == -1) {
- for (size_t i = 0; i < n; i++) {
- free(items[i]);
- free(names[i]);
- }
- free(items);
- free(names);
- free(ids);
- free(active);
+ clist_clear(&l);
return;
}
if (sel == -2) {
fetch = 1;
continue;
}
- if (sel == -6 && cur >= 0 && (size_t)cur < n) {
- customer_archive_toggle(a, ids[cur], names[cur],
- active[cur] != 0);
+ if (sel == -6 && cur >= 0 && (size_t)cur < l.nrows) {
+ if (l.ids[cur] == 0) {
+ if (l.dkeys[cur] && tui_confirm("Kund", "Radera utkastet?"))
+ drafts_del(kdrafts(), a->org, "customer", l.dkeys[cur]);
+ } else {
+ customer_archive_toggle(a, l.ids[cur], l.names[cur],
+ l.active[cur] != 0);
+ }
fetch = 1;
continue;
}
- if (sel == -4 || (sel >= 0 && (size_t)sel == n)) {
- customer_form(a, 0);
+ if (sel == -4 || (cur >= 0 && (size_t)cur == l.nrows)) {
+ char *tid = drafts_new_id();
+ drafts_put(kdrafts(), a->org, "customer", tid, "{}");
+ customer_form(a, 0, tid);
+ free(tid);
fetch = 1;
continue;
}
- if (sel >= 0 && (size_t)sel < n) {
- customer_form(a, ids[sel]);
+ if (sel >= 0 && (size_t)sel < l.nrows) {
+ char idbuf[32];
+ const char *dk = l.dkeys[sel];
+ if (!dk) {
+ snprintf(idbuf, sizeof idbuf, "%lld", (long long)l.ids[sel]);
+ dk = idbuf;
+ }
+ customer_form(a, l.ids[sel], dk);
fetch = 1;
continue;
}
@@ -457,9 +721,13 @@ struct invctx {
int64_t id;
int64_t number;
int64_t customer_id;
+ int64_t total_ore;
char customer[256];
+ char paid_date[16];
};
+static int64_t invoices_new_from(struct app *a, int64_t id);
+
static void inv_detail_pdf(struct invctx *ctx)
{
char args[64];
@@ -476,7 +744,7 @@ static void inv_detail_pdf(struct invctx *ctx)
char fname[800];
snprintf(fname, sizeof fname, "Faktura %lld %s.pdf",
(long long)ctx->number, ctx->customer);
- pdf_save_and_open(b64, fname, "Faktura PDF");
+ file_save_and_open(b64, fname, "Faktura PDF");
free(b64);
}
free(resp);
@@ -523,6 +791,61 @@ static void inv_detail_send(struct invctx *ctx)
free(r);
}
+/* Prefills the new-invoice form with the payment voucher: D the org's
+ bank account, K the invoice receivable account, both editable. */
+static void inv_detail_pay(struct invctx *ctx)
+{
+ struct app *a = ctx->a;
+ if (ctx->paid_date[0]) {
+ tui_message("Kvittera betalning", "Fakturan är redan betald.");
+ return;
+ }
+ char pay[16] = "1930", rec[16] = "1510";
+ char *resp = client_rpc(&a->conn, "settings.get", a->session, a->org,
+ "{}");
+ if (resp && client_ok(resp)) {
+ char *v = jstr_dup(resp, "result.bank_account");
+ if (v && *v)
+ snprintf(pay, sizeof pay, "%s", v);
+ free(v);
+ v = jstr_dup(resp, "result.invoice_receivable_account");
+ if (v && *v)
+ snprintf(rec, sizeof rec, "%s", v);
+ free(v);
+ }
+ free(resp);
+ char desc[160];
+ snprintf(desc, sizeof desc, "Betalning faktura %lld %.120s",
+ (long long)ctx->number, ctx->customer);
+ struct voucher_prefill p;
+ memset(&p, 0, sizeof p);
+ char date[16];
+ today_iso(date, sizeof date);
+ p.date = date;
+ p.description = desc;
+ p.bank_account = pay;
+ p.counter_account = rec;
+ p.amount_ore = ctx->total_ore;
+ int64_t vid = vouchers_new_prefill(a, &p);
+ if (vid <= 0)
+ return;
+ char args[96];
+ snprintf(args, sizeof args, "{\"id\":%lld,\"voucher_id\":%lld}",
+ (long long)ctx->id, (long long)vid);
+ resp = client_rpc(&a->conn, "invoice.pay", a->session, a->org, args);
+ if (resp && client_ok(resp)) {
+ char *ser = jstr_dup(resp, "result.voucher_series");
+ int64_t num = jint_val(resp, "result.voucher_number", 0);
+ tui_message("Kvittera betalning", "Faktura %lld kvitterad (%s%lld).",
+ (long long)ctx->number, ser ? ser : "",
+ (long long)num);
+ free(ser);
+ } else {
+ show_error("Kvittera betalning", resp);
+ }
+ free(resp);
+}
+
static int inv_key(void *ud, int ch)
{
struct invctx *ctx = ud;
@@ -534,6 +857,14 @@ static int inv_key(void *ud, int ch)
inv_detail_send(ctx);
return TUI_HOOK_STAY;
}
+ if (ch == 'u') {
+ invoices_new_from(ctx->a, ctx->id);
+ return TUI_HOOK_REFRESH;
+ }
+ if (ch == 'b') {
+ inv_detail_pay(ctx);
+ return TUI_HOOK_REFRESH;
+ }
return TUI_HOOK_NONE;
}
@@ -565,10 +896,19 @@ static void invoices_detail(struct app *a, int64_t id)
char *ocr = jstr_dup(resp, "result.ocr");
char *sent_at = jstr_dup(resp, "result.last_sent_at");
char *sent_to = jstr_dup(resp, "result.last_sent_to");
+ char *paid_date = jstr_dup(resp, "result.paid_date");
+ char *paid_ser = jstr_dup(resp, "result.paid_voucher_series");
+ int64_t paid_num = jint_val(resp, "result.paid_voucher_number", 0);
+ char statbuf[96];
+ if (paid_date && *paid_date)
+ snprintf(statbuf, sizeof statbuf, "betald %s", paid_date);
+ else
+ snprintf(statbuf, sizeof statbuf, "%s",
+ invoice_status_sv(status));
struct buf t;
buf_init(&t);
buf_line(&t, MK_HEAD "Faktura %lld %s\n", (long long)number,
- invoice_status_sv(status));
+ statbuf);
buf_line(&t, MK_DIM "%s\n\n", cust ? cust : "");
buf_line(&t, "Fakturadatum: %s\n", date ? date : "");
buf_line(&t, "Förfallodatum: %s\n", due ? due : "");
@@ -584,6 +924,9 @@ static void invoices_detail(struct app *a, int64_t id)
buf_line(&t, "Skickad: %s%s%s\n", sent_at,
sent_to && *sent_to ? " till " : "",
sent_to && *sent_to ? sent_to : "");
+ if (paid_ser && *paid_ser && paid_num > 0)
+ buf_line(&t, "Betalningsverifikat: %s%lld\n", paid_ser,
+ (long long)paid_num);
if (notes && *notes)
buf_line(&t, "\nFritext:\n%s\n", notes);
buf_line(&t, "\n");
@@ -609,6 +952,18 @@ static void invoices_detail(struct app *a, int64_t id)
char *note = jstr_dup(resp, path);
snprintf(path, sizeof path, "result.rows.%zu.amount_ore", i);
int64_t amount = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.rows.%zu.is_text", i);
+ if (jbool_val(resp, path, 0)) {
+ char dbuf[256];
+ snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : "");
+ tui_pad_field(dbuf, sizeof dbuf, 34);
+ buf_line(&t, "%s\n", dbuf);
+ free(desc);
+ free(unit);
+ free(vat_code);
+ free(note);
+ continue;
+ }
char dbuf[256], qbuf[24], qcol[24], ubuf[24], pcol[40],
vbuf[24], nbuf[128], acol[40];
snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : "");
@@ -644,10 +999,25 @@ static void invoices_detail(struct app *a, int64_t id)
ctx.id = id;
ctx.number = number;
ctx.customer_id = customer_id;
+ ctx.total_ore = total;
snprintf(ctx.customer, sizeof ctx.customer, "%s", cust ? cust : "");
- int ret = tui_pager_hook("Faktura", (const char *)t.p,
- "p = visa PDF s = skicka", 0, inv_key,
- &ctx);
+ if (paid_date && *paid_date)
+ snprintf(ctx.paid_date, sizeof ctx.paid_date, "%s", paid_date);
+ char ahint[192];
+ snprintf(ahint, sizeof ahint,
+ "p = visa PDF s = skicka u = duplicera%s",
+ ctx.paid_date[0] ? "" : " b = kvittera betalning");
+ struct tui_action acts[] = {
+ { "invoice.pdf", "Visa PDF", 'p', 1, NULL },
+ { "invoice.send", "Skicka…", 's', 1, NULL },
+ { "invoice.duplicate", "Duplicera till nytt utkast", 'u', 1,
+ NULL },
+ { "invoice.pay", "Kvittera betalning…", 'b',
+ ctx.paid_date[0] ? 0 : 1, "redan betald" },
+ };
+ tui_set_actions(acts, 4);
+ int ret = tui_pager_hook("Faktura", (const char *)t.p, ahint, 0,
+ inv_key, &ctx);
free(cust);
free(status);
free(date);
@@ -659,6 +1029,8 @@ static void invoices_detail(struct app *a, int64_t id)
free(ocr);
free(sent_at);
free(sent_to);
+ free(paid_date);
+ free(paid_ser);
buf_free(&t);
free(resp);
if (ret != 1)
@@ -890,7 +1262,7 @@ static char *iform_draft_args(struct iform *f, char *err, size_t errn)
yyjson_mut_obj_add_strcpy(d, o, "our_ref", f->our_ref);
yyjson_mut_obj_add_strcpy(d, o, "notes", f->notes);
yyjson_mut_val *arr = yyjson_mut_arr(d);
- int used = 0;
+ int used = 0, priced = 0;
for (int i = 0; i < f->rt.nrows; i++) {
struct irow *r = &f->rows[i];
if (!r->description[0] && !r->quantity[0] && !r->unit[0] &&
@@ -901,6 +1273,13 @@ static char *iform_draft_args(struct iform *f, char *err, size_t errn)
yyjson_mut_doc_free(d);
return NULL;
}
+ if (!r->quantity[0] && !r->unit[0] && !r->price[0] && !r->vat[0]) {
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
+ yyjson_mut_obj_add_strcpy(d, ro, "description", r->description);
+ yyjson_mut_obj_add_bool(d, ro, "text", true);
+ used++;
+ continue;
+ }
const char *qty = r->quantity[0] ? r->quantity : "1";
if (!qty_is_valid(qty)) {
snprintf(err, errn,
@@ -937,12 +1316,19 @@ static char *iform_draft_args(struct iform *f, char *err, size_t errn)
yyjson_mut_obj_add_strcpy(d, ro, "note", r->note);
yyjson_mut_obj_add_strcpy(d, ro, "vat_code", vat);
used++;
+ priced++;
}
if (used == 0) {
snprintf(err, errn, "Minst en rad krävs.");
yyjson_mut_doc_free(d);
return NULL;
}
+ if (priced == 0) {
+ snprintf(err, errn,
+ "Minst en prissatt rad krävs (fritextrader saknar belopp).");
+ yyjson_mut_doc_free(d);
+ return NULL;
+ }
yyjson_mut_obj_add_val(d, o, "rows", arr);
char *out = yyjson_mut_write(d, 0, NULL);
yyjson_mut_doc_free(d);
@@ -967,7 +1353,7 @@ static void iform_preview(struct iform *f)
}
char *b64 = jstr_dup(resp, "result.content_base64");
if (b64) {
- pdf_save_and_open(b64, "Faktura förhandsvisning.pdf", "Faktura PDF");
+ file_save_and_open(b64, "Faktura förhandsvisning.pdf", "Faktura PDF");
free(b64);
}
free(resp);
@@ -1021,33 +1407,8 @@ static int64_t iform_issue(struct iform *f)
return id;
}
-static int64_t invoices_new(struct app *a)
+static int64_t invoices_form_run(struct iform *f)
{
- struct iform f;
- memset(&f, 0, sizeof f);
- f.a = a;
- today_iso(f.date, sizeof f.date);
- snprintf(f.delivery, sizeof f.delivery, "%s", f.date);
- snprintf(f.due, sizeof f.due, "%s", f.date);
- snprintf(f.auto_due, sizeof f.auto_due, "%s", f.due);
- if (iform_load_customers(&f) != 0) {
- iform_customers_free(&f);
- return 0;
- }
- char *resp =
- client_rpc(&a->conn, "settings.get", a->session, a->org, "{}");
- if (resp && client_ok(resp)) {
- char *v = jstr_dup(resp, "result.invoice_our_ref");
- if (v) {
- snprintf(f.our_ref, sizeof f.our_ref, "%s", v);
- free(v);
- }
- }
- free(resp);
- if (f.ncust > 0) {
- f.cust_sel = 0;
- iform_customer_changed(&f);
- }
struct tui_rt_col cols[6] = {
{ "Beskrivning", 2, 34, TUI_RT_EDIT },
{ "Antal", 37, 8, TUI_RT_EDIT },
@@ -1056,66 +1417,196 @@ static int64_t invoices_new(struct app *a)
{ "Moms", 68, 14, TUI_RT_EDIT },
{ "Anm", 83, 32, TUI_RT_EDIT },
};
- tui_rt_init(&f.rt, 1, IFORM_ROWS, 6, cols, 0, iform_cell, NULL, &f);
- tui_rt_set_key(&f.rt, iform_key);
+ tui_rt_init(&f->rt, 1, IFORM_ROWS, 6, cols, 0, iform_cell, NULL, f);
+ tui_rt_set_key(&f->rt, iform_key);
struct tui_form_field ff[7];
memset(ff, 0, sizeof ff);
ff[0].label = "Kund";
ff[0].kind = TUI_F_CHOICE;
- ff[0].choices = (const char *const *)f.cust_names;
- ff[0].nchoices = (int)f.ncust;
- ff[0].choice = &f.cust_sel;
+ ff[0].choices = (const char *const *)f->cust_names;
+ ff[0].nchoices = (int)f->ncust;
+ ff[0].choice = &f->cust_sel;
ff[1].label = "Fakturadatum";
- ff[1].value = f.date;
- ff[1].cap = sizeof f.date;
+ ff[1].value = f->date;
+ ff[1].cap = sizeof f->date;
ff[1].kind = TUI_F_DATE;
ff[2].label = "Förfallodatum";
- ff[2].value = f.due;
- ff[2].cap = sizeof f.due;
+ ff[2].value = f->due;
+ ff[2].cap = sizeof f->due;
ff[2].kind = TUI_F_DATE;
ff[3].label = "Leveransdatum";
- ff[3].value = f.delivery;
- ff[3].cap = sizeof f.delivery;
+ ff[3].value = f->delivery;
+ ff[3].cap = sizeof f->delivery;
ff[3].kind = TUI_F_DATE;
ff[4].label = "Er referens";
- ff[4].value = f.your_ref;
- ff[4].cap = sizeof f.your_ref;
+ ff[4].value = f->your_ref;
+ ff[4].cap = sizeof f->your_ref;
ff[4].kind = TUI_F_TEXT;
ff[5].label = "Vår referens";
- ff[5].value = f.our_ref;
- ff[5].cap = sizeof f.our_ref;
+ ff[5].value = f->our_ref;
+ ff[5].cap = sizeof f->our_ref;
ff[5].kind = TUI_F_TEXT;
ff[6].label = "Fritext";
- ff[6].value = f.notes;
- ff[6].cap = sizeof f.notes;
+ ff[6].value = f->notes;
+ ff[6].cap = sizeof f->notes;
ff[6].kind = TUI_F_TEXT;
- tui_rt_set_fields(&f.rt, ff, 7);
+ tui_rt_set_fields(&f->rt, ff, 7);
+ tui_rt_normalize(&f->rt);
const char *hint =
- "Enter = välj/ändra Tab = byta fält F5 = förhandsvisa"
- " ^Enter/F9 = utfärda Esc = avbryt";
+ "Enter = välj/ändra Tab = byta fält"
+ " ^O = åtgärder (utfärda, förhandsvisa) Esc = avbryt";
+ static const struct tui_action acts[] = {
+ { "invoice.issue", "Utfärda", TUI_KEY_SUBMIT, 1, NULL },
+ { "invoice.preview", "Förhandsvisa PDF", TUI_KEY_REFRESH, 1, NULL },
+ };
int64_t out = 0;
for (;;) {
- int rr = tui_rt_run("Ny faktura", &f.rt, hint);
+ tui_set_actions(acts, 2);
+ int rr = tui_rt_run("Ny faktura", &f->rt, hint);
if (rr == -1)
break;
if (rr == -2) {
- iform_preview(&f);
+ iform_preview(f);
continue;
}
if (rr == -3) {
- out = iform_issue(&f);
+ out = iform_issue(f);
if (out > 0)
break;
}
}
+ return out;
+}
+
+static int64_t invoices_new(struct app *a)
+{
+ struct iform f;
+ memset(&f, 0, sizeof f);
+ f.a = a;
+ today_iso(f.date, sizeof f.date);
+ snprintf(f.delivery, sizeof f.delivery, "%s", f.date);
+ snprintf(f.due, sizeof f.due, "%s", f.date);
+ snprintf(f.auto_due, sizeof f.auto_due, "%s", f.due);
+ if (iform_load_customers(&f) != 0) {
+ iform_customers_free(&f);
+ return 0;
+ }
+ char *resp =
+ client_rpc(&a->conn, "settings.get", a->session, a->org, "{}");
+ if (resp && client_ok(resp)) {
+ char *v = jstr_dup(resp, "result.invoice_our_ref");
+ if (v) {
+ snprintf(f.our_ref, sizeof f.our_ref, "%s", v);
+ free(v);
+ }
+ }
+ free(resp);
+ if (f.ncust > 0) {
+ f.cust_sel = 0;
+ iform_customer_changed(&f);
+ }
+ int64_t out = invoices_form_run(&f);
iform_customers_free(&f);
return out;
}
+/* Duplicates an issued invoice: same customer, rows and references, with
+ the dates reset (invoice and delivery today, due today + payment days). */
+static int64_t invoices_new_from(struct app *a, int64_t id)
+{
+ char iargs[64];
+ snprintf(iargs, sizeof iargs, "{\"id\":%lld}", (long long)id);
+ char *resp =
+ client_rpc(&a->conn, "invoice.get", a->session, a->org, iargs);
+ if (!resp || !client_ok(resp)) {
+ show_error("Faktura", resp);
+ free(resp);
+ return 0;
+ }
+ struct iform f;
+ memset(&f, 0, sizeof f);
+ f.a = a;
+ today_iso(f.date, sizeof f.date);
+ snprintf(f.delivery, sizeof f.delivery, "%s", f.date);
+ snprintf(f.due, sizeof f.due, "%s", f.date);
+ snprintf(f.auto_due, sizeof f.auto_due, "%s", f.due);
+ if (iform_load_customers(&f) != 0) {
+ iform_customers_free(&f);
+ free(resp);
+ return 0;
+ }
+ int64_t cid = jint_val(resp, "result.customer_id", 0);
+ int found = -1;
+ for (size_t i = 0; i < f.ncust; i++)
+ if (f.cust_ids[i] == cid) {
+ found = (int)i;
+ break;
+ }
+ if (found < 0) {
+ tui_message("Ny faktura",
+ "Kunden är arkiverad eller finns inte kvar.");
+ iform_customers_free(&f);
+ free(resp);
+ return 0;
+ }
+ f.cust_sel = found;
+ iform_customer_changed(&f);
+ char *v = jstr_dup(resp, "result.our_ref");
+ if (v) {
+ snprintf(f.our_ref, sizeof f.our_ref, "%s", v);
+ free(v);
+ }
+ v = jstr_dup(resp, "result.notes");
+ if (v) {
+ snprintf(f.notes, sizeof f.notes, "%s", v);
+ free(v);
+ }
+ size_t nrows = jarr_size(resp, "result.rows");
+ if (nrows > IFORM_ROWS)
+ nrows = IFORM_ROWS;
+ for (size_t i = 0; i < nrows; i++) {
+ struct irow *r = &f.rows[i];
+ char path[64];
+ snprintf(path, sizeof path, "result.rows.%zu.description", i);
+ char *desc = jstr_dup(resp, path);
+ if (desc)
+ snprintf(r->description, sizeof r->description, "%s", desc);
+ snprintf(path, sizeof path, "result.rows.%zu.is_text", i);
+ if (jbool_val(resp, path, 0)) {
+ free(desc);
+ continue;
+ }
+ snprintf(path, sizeof path, "result.rows.%zu.quantity_milli", i);
+ int64_t qm = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.rows.%zu.unit", i);
+ char *unit = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.unit_price_ore", i);
+ int64_t price = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.rows.%zu.vat_code", i);
+ char *vat = jstr_dup(resp, path);
+ qty_sv(qm, r->quantity, sizeof r->quantity);
+ if (unit)
+ snprintf(r->unit, sizeof r->unit, "%s", unit);
+ char pbuf[24];
+ tui_kr_format(price, pbuf, sizeof pbuf);
+ snprintf(r->price, sizeof r->price, "%s", pbuf);
+ snprintf(r->vat, sizeof r->vat, "%s", vat_label(vat));
+ free(desc);
+ free(unit);
+ free(vat);
+ }
+ free(resp);
+ int64_t out = invoices_form_run(&f);
+ iform_customers_free(&f);
+ return out;
+}
+
+#define INVOICES_NEXT_NUMBER TUI_KEY_ACTION(0)
+
static int invoices_key(void *ud, int ch)
{
struct app *a = ud;
- if (ch != 'n')
+ if (ch != INVOICES_NEXT_NUMBER)
return TUI_HOOK_NONE;
char *resp = client_rpc(&a->conn, "invoice.sequence_get", a->session,
a->org, "{}");
@@ -1187,21 +1678,28 @@ void invoices_screen(struct app *a)
int64_t total = jint_val(resp, path, 0);
snprintf(path, sizeof path, "result.items.%zu.status", i);
char *status = jstr_dup(resp, path);
- char nbuf[32], cbuf[256], abuf[40];
+ snprintf(path, sizeof path, "result.items.%zu.paid_date", i);
+ char *paid = jstr_dup(resp, path);
+ char nbuf[32], cbuf[256], abuf[40], stbuf[64];
snprintf(nbuf, sizeof nbuf, "Faktura %lld", (long long)number);
snprintf(cbuf, sizeof cbuf, "%s", cust ? cust : "");
tui_pad_field(cbuf, sizeof cbuf, 28);
tui_amt_col(abuf, sizeof abuf, 14, total);
+ if (paid && *paid)
+ snprintf(stbuf, sizeof stbuf, "betald %s", paid);
+ else
+ snprintf(stbuf, sizeof stbuf, "%s",
+ invoice_status_sv(status));
snprintf(line, sizeof line, "%s %s %s %s %s", nbuf,
- date ? date : "", cbuf, abuf,
- invoice_status_sv(status));
+ date ? date : "", cbuf, abuf, stbuf);
items[i] = xstrdup(line);
free(date);
free(cust);
free(status);
+ free(paid);
}
free(resp);
- items[n] = xstrdup("+ Ny faktura (^N)");
+ items[n] = xstrdup("+ Ny faktura (n)");
}
int start = 0;
if (a->invoice_sel) {
@@ -1212,6 +1710,12 @@ void invoices_screen(struct app *a)
}
}
int cur = start;
+ static const struct tui_action lacts[] = {
+ { "invoice.new", "Ny faktura", 'n', 1, NULL },
+ { "invoice.next_number", "Ändra nästa fakturanummer…",
+ INVOICES_NEXT_NUMBER, 1, NULL },
+ };
+ tui_set_actions(lacts, 2);
int sel = tui_select_list_hook("Fakturor", items, (int)n + 1, start, 1,
&cur, 1, NULL, 0, invoices_key, a);
if (cur >= 0 && (size_t)cur < n)