Compare commits

...

6 Commits

Author SHA1 Message Date
677d6c9797 implemented creation of new account
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-02 00:35:07 +02:00
153584a031 preparing to save data for accouting
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-01 23:51:09 +02:00
15f8116430 working on entry form
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
2026-04-01 23:26:04 +02:00
bde0901302 preparing for new accounting module 2026-04-01 18:28:02 +02:00
9f5e1e0853 Merge branch 'main' into dev 2026-04-01 18:14:24 +02:00
55dfea65b0 Merge branch 'bugfix/wiki-css' into dev
All checks were successful
Build Docker Image / Docker-Build (push) Successful in 2m34s
Build Docker Image / Clean-Registry (push) Successful in -12s
2026-03-30 23:59:17 +02:00
20 changed files with 379 additions and 38 deletions

View File

@@ -0,0 +1,5 @@
description = "Umbrella : Accounting"
dependencies{
implementation(project(":core"))
}

View File

@@ -0,0 +1,7 @@
package de.srsoftware.umbrella.accounting;
import de.srsoftware.umbrella.core.model.Account;
public interface AccountDb {
Account save(Account account);
}

View File

@@ -0,0 +1,85 @@
package de.srsoftware.umbrella.accounting;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.AccountingService;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Account;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import org.json.JSONObject;
import java.io.IOException;
import java.util.Optional;
import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.umbrella.accounting.Constants.CONFIG_DATABASE;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.constants.Path.JSON;
import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
public class AccountingModule extends BaseHandler implements AccountingService {
private final AccountDb accountDb;
public AccountingModule(Configuration config) throws UmbrellaException {
super();
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE));
accountDb = new SqliteDb(connect(dbFile));
ModuleRegistry.add(this);
}
@Override
public boolean doPost(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = ModuleRegistry.userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case null -> postEntry(user.get(),ex);
default -> super.doPost(path,ex);
};
} catch (NumberFormatException e){
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
} catch (UmbrellaException e){
return send(ex,e);
}
}
private boolean postEntry(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT);
if (!(json.get(Field.ACCOUNT) instanceof JSONObject acc)) throw invalidField(Field.ACCOUNT,JSON);
// TODO more tests
Long accountId = null;
if (acc.has(Field.ID)) {
if (!(acc.get(Field.ID) instanceof Number accId)) throw invalidField(String.join(".",Field.ACCOUNT,Field.ID), Text.NUMBER);
if (accId.longValue() != 0) accountId = accId.longValue();
}
if (accountId == null){
if (!acc.has(Field.NAME)) throw missingField(String.join(".",Field.ACCOUNT,Field.NAME));
var accountName = acc.getString(Field.NAME);
if (accountName.isBlank()) throw invalidField(String.join(".",Field.ACCOUNT,Field.NAME),Text.STRING);
var currency = acc.has(Field.CURRENCY) ? nullIfEmpty(acc.getString(Field.CURRENCY)) : null;
var account = accountDb.save(new Account(0, accountName, currency, user.id()));
accountId = account.id();
}
// TODO: save entry
return notFound(ex);
}
}

View File

@@ -0,0 +1,8 @@
package de.srsoftware.umbrella.accounting;
public class Constants {
public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database";
public static final String TABLE_ACCOUNTS = "accounts";
public static final String TABLE_TRANSACTIONS = "transactions";
}

View File

@@ -0,0 +1,88 @@
package de.srsoftware.umbrella.accounting;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.model.Account;
import java.sql.Connection;
import java.sql.SQLException;
import static de.srsoftware.tools.NotImplemented.notImplemented;
import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS;
import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToCreateTable;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToStoreObject;
import static java.text.MessageFormat.format;
public class SqliteDb extends BaseDb implements AccountDb {
public SqliteDb(Connection connection) {
super(connection);
}
@Override
protected int createTables() {
var version = createSettingsTable();
switch (version){
case 0:
createAccountsTable();
createTransactionsTable();
}
return setCurrentVersion(1);
}
private void createAccountsTable() {
var sql = """
CREATE TABLE IF NOT EXISTS {0} (
{1} INTEGER PRIMARY KEY,
{2} VARCHAR(255) NOT NULL,
{3} LONG NOT NULL,
{4} CURRENCY VARCHAR(16)
);""";
try {
sql = format(sql,TABLE_ACCOUNTS, Field.ID, Field.NAME, Field.OWNER, Field.CURRENCY);
var stmt = db.prepareStatement(sql);
stmt.execute();
stmt.close();
} catch (SQLException e) {
throw failedToCreateTable(TABLE_ACCOUNTS).causedBy(e);
}
}
private void createTransactionsTable() {
var sql = """
CREATE TABLE IF NOT EXISTS {0} (
{1} LONG NOT NULL,
{2} LONG NOT NULL,
{3} VARCHAR(255) NOT NULL,
{4} VARCHAR(255) NOT NULL,
{5} DOUBLE NOT NULL,
{6} TEXT
);""";
try {
sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION);
var stmt = db.prepareStatement(sql);
stmt.execute();
stmt.close();
} catch (SQLException e) {
throw failedToCreateTable(TABLE_TRANSACTIONS).causedBy(e);
}
}
@Override
public Account save(Account account) {
if (account.id() == 0) try { // new account
var rs = Query.insertInto(TABLE_ACCOUNTS,Field.NAME, Field.CURRENCY, Field.OWNER).values(account.name(),account.currency(),account.ownerId()).execute(db).getGeneratedKeys();
Long newId = null;
if (rs.next()) newId = rs.getLong(1);
rs.close();
if (newId == null) throw failedToStoreObject(account);
return account.withId(newId);
} catch (SQLException e) {
throw failedToStoreObject(account).causedBy(e);
} else {
throw notImplemented(this,"save(account)");
}
}
}

View File

@@ -11,6 +11,7 @@ application{
} }
dependencies{ dependencies{
implementation(project(":accounting"));
implementation(project(":bookmark")); implementation(project(":bookmark"));
implementation(project(":bus")); implementation(project(":bus"));
implementation(project(":company")) implementation(project(":company"))
@@ -47,6 +48,7 @@ tasks.jar {
.map(::zipTree) // OR .map { zipTree(it) } .map(::zipTree) // OR .map { zipTree(it) }
from(dependencies) from(dependencies)
dependsOn( dependsOn(
":accounting:jar",
":bookmark:jar", ":bookmark:jar",
":bus:jar", ":bus:jar",
":company:jar", ":company:jar",

View File

@@ -9,11 +9,13 @@ import static java.lang.System.Logger.Level.INFO;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import de.srsoftware.configuration.JsonConfig; import de.srsoftware.configuration.JsonConfig;
import de.srsoftware.tools.ColorLogger; import de.srsoftware.tools.ColorLogger;
import de.srsoftware.umbrella.accounting.AccountingModule;
import de.srsoftware.umbrella.bookmarks.BookmarkApi; import de.srsoftware.umbrella.bookmarks.BookmarkApi;
import de.srsoftware.umbrella.company.CompanyModule; import de.srsoftware.umbrella.company.CompanyModule;
import de.srsoftware.umbrella.contact.ContactModule; import de.srsoftware.umbrella.contact.ContactModule;
import de.srsoftware.umbrella.core.SettingsService; import de.srsoftware.umbrella.core.SettingsService;
import de.srsoftware.umbrella.core.Util; import de.srsoftware.umbrella.core.Util;
import de.srsoftware.umbrella.core.api.AccountingService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.documents.DocumentApi; import de.srsoftware.umbrella.documents.DocumentApi;
import de.srsoftware.umbrella.files.FileModule; import de.srsoftware.umbrella.files.FileModule;
@@ -92,6 +94,7 @@ public class Application {
new WikiModule(config).bindPath("/api/wiki").on(server); new WikiModule(config).bindPath("/api/wiki").on(server);
new FileModule(config).bindPath("/api/files").on(server); new FileModule(config).bindPath("/api/files").on(server);
new SettingsService(config).bindPath("/api/settings").on(server); new SettingsService(config).bindPath("/api/settings").on(server);
new AccountingModule(config).bindPath("/api/accounting").on(server);
} catch (Exception e) { } catch (Exception e) {
LOG.log(ERROR,"Startup failed",e); LOG.log(ERROR,"Startup failed",e);
System.exit(-1); System.exit(-1);

View File

@@ -5,6 +5,7 @@ package de.srsoftware.umbrella.core;
import de.srsoftware.umbrella.core.api.*; import de.srsoftware.umbrella.core.api.*;
public class ModuleRegistry { public class ModuleRegistry {
private AccountingService accountingService;
private BookmarkService bookmarkService; private BookmarkService bookmarkService;
private CompanyService companyService; private CompanyService companyService;
private ContactService contactService; private ContactService contactService;
@@ -29,23 +30,24 @@ public class ModuleRegistry {
public static void add(Object service) { public static void add(Object service) {
switch (service) { switch (service) {
case BookmarkService bs: singleton.bookmarkService = bs; break; case AccountingService as: singleton.accountingService = as; break;
case CompanyService cs: singleton.companyService = cs; break; case BookmarkService bs: singleton.bookmarkService = bs; break;
case ContactService cs: singleton.contactService = cs; break; case CompanyService cs: singleton.companyService = cs; break;
case DocumentService ds: singleton.documentService = ds; break; case ContactService cs: singleton.contactService = cs; break;
case FileService fs: singleton.fileService = fs; break; case DocumentService ds: singleton.documentService = ds; break;
case StockService is: singleton.stockService = is; break; case FileService fs: singleton.fileService = fs; break;
case MarkdownService ms: singleton.markdownService = ms; break; case StockService is: singleton.stockService = is; break;
case NoteService ns: singleton.noteService = ns; break; case MarkdownService ms: singleton.markdownService = ms; break;
case PollService ps: singleton.pollService = ps; break; case NoteService ns: singleton.noteService = ns; break;
case PostBox pb: singleton.postBox = pb; break; case PollService ps: singleton.pollService = ps; break;
case ProjectService ps: singleton.projectService = ps; break; case PostBox pb: singleton.postBox = pb; break;
case TagService ts: singleton.tagService = ts; break; case ProjectService ps: singleton.projectService = ps; break;
case TaskService ts: singleton.taskService = ts; break; case TagService ts: singleton.tagService = ts; break;
case TimeService ts: singleton.timeService = ts; break; case TaskService ts: singleton.taskService = ts; break;
case Translator tr: singleton.translator = tr; break; case TimeService ts: singleton.timeService = ts; break;
case UserService us: singleton.userService = us; break; case Translator tr: singleton.translator = tr; break;
case WikiService ws: singleton.wikiService = ws; break; case UserService us: singleton.userService = us; break;
case WikiService ws: singleton.wikiService = ws; break;
case null: break; case null: break;
default: System.getLogger(ModuleRegistry.class.getSimpleName()).log(System.Logger.Level.WARNING,"Trying to add untracked class {0}",service.getClass().getSimpleName()); default: System.getLogger(ModuleRegistry.class.getSimpleName()).log(System.Logger.Level.WARNING,"Trying to add untracked class {0}",service.getClass().getSimpleName());
} }

View File

@@ -85,6 +85,7 @@ public class SettingsService extends BaseHandler {
entries.add(MenuEntry.of(13,Module.STOCK)); entries.add(MenuEntry.of(13,Module.STOCK));
entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES)); entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES));
entries.add(MenuEntry.of(15,Module.POLL,Text.POLLS)); entries.add(MenuEntry.of(15,Module.POLL,Text.POLLS));
entries.add(MenuEntry.of(16,Module.ACCOUNTING,Text.ACCOUNTING));
for (var i=0; i<entries.size(); i++){ for (var i=0; i<entries.size(); i++){
var entry = entries.get(i); var entry = entries.get(i);

View File

@@ -0,0 +1,4 @@
package de.srsoftware.umbrella.core.api;
public interface AccountingService {
}

View File

@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core.constants;
public class Field { public class Field {
public static final String ACTION = "action"; public static final String ACTION = "action";
public static final String ACCOUNT = "account";
public static final String ADDRESS = "address"; public static final String ADDRESS = "address";
public static final String ALLOWED_STATES = "allowed_states"; public static final String ALLOWED_STATES = "allowed_states";
public static final String AMOUNT = "amount"; public static final String AMOUNT = "amount";
@@ -43,6 +44,7 @@ public class Field {
public static final String DELIVERY = "delivery"; public static final String DELIVERY = "delivery";
public static final String DELIVERY_DATE = "delivery_date"; public static final String DELIVERY_DATE = "delivery_date";
public static final String DESCRIPTION = "description"; public static final String DESCRIPTION = "description";
public static final String DESTINATION = "destination";
public static final String DOCUMENT = "document"; public static final String DOCUMENT = "document";
public static final String DOCUMENT_ID = "document_id"; public static final String DOCUMENT_ID = "document_id";
public static final String DOC_TYPE_ID = "document_type_id"; public static final String DOC_TYPE_ID = "document_type_id";

View File

@@ -2,19 +2,20 @@
package de.srsoftware.umbrella.core.constants; package de.srsoftware.umbrella.core.constants;
public class Module { public class Module {
public static final String BOOKMARK = "bookmark"; public static final String ACCOUNTING = "accounting";
public static final String COMPANY = "company"; public static final String BOOKMARK = "bookmark";
public static final String CONTACT = "contact"; public static final String COMPANY = "company";
public static final String DOCUMENT = "document"; public static final String CONTACT = "contact";
public static final String FILES = "files"; public static final String DOCUMENT = "document";
public static final String MESSAGE = "message"; public static final String FILES = "files";
public static final String NOTES = "notes"; public static final String MESSAGE = "message";
public static final String POLL = "poll"; public static final String NOTES = "notes";
public static final String PROJECT = "project"; public static final String POLL = "poll";
public static final String STOCK = "stock"; public static final String PROJECT = "project";
public static final String TAGS = "tags"; public static final String STOCK = "stock";
public static final String TASK = "task"; public static final String TAGS = "tags";
public static final String TIME = "time"; public static final String TASK = "task";
public static final String USER = "user"; public static final String TIME = "time";
public static final String WIKI = "wiki"; public static final String USER = "user";
public static final String WIKI = "wiki";
} }

View File

@@ -5,6 +5,8 @@ package de.srsoftware.umbrella.core.constants;
* This is a collection of messages that appear throughout the project * This is a collection of messages that appear throughout the project
*/ */
public class Text { public class Text {
public static final String ACCOUNTING = "accounting";
public static final String BOOKMARK = "bookmark"; public static final String BOOKMARK = "bookmark";
public static final String BOOKMARKS = "bookmarks"; public static final String BOOKMARKS = "bookmarks";
public static final String BOOLEAN = "Boolean"; public static final String BOOLEAN = "Boolean";
@@ -47,8 +49,8 @@ public class Text {
public static final String NOTE_WITH_ID = "note ({id})"; public static final String NOTE_WITH_ID = "note ({id})";
public static final String NUMBER = "number"; public static final String NUMBER = "number";
public static final Object OPTION = "option" public static final Object OPTION = "option";
;
public static final String PATH = "path"; public static final String PATH = "path";
public static final String PERMISSION = "permission"; public static final String PERMISSION = "permission";
public static final String POLL = "poll"; public static final String POLL = "poll";

View File

@@ -0,0 +1,7 @@
package de.srsoftware.umbrella.core.model;
public record Account(long id, String name, String currency, long ownerId) {
public Account withId(long newId) {
return new Account(newId,name,currency,ownerId);
}
}

View File

@@ -5,6 +5,7 @@
import { loadTranslation } from './translations.svelte'; import { loadTranslation } from './translations.svelte';
import { checkUser, user } from './user.svelte'; import { checkUser, user } from './user.svelte';
import Accounts from "./routes/accounting/index.svelte";
import AddDoc from "./routes/document/Add.svelte"; import AddDoc from "./routes/document/Add.svelte";
import AddTask from "./routes/task/Add.svelte"; import AddTask from "./routes/task/Add.svelte";
import Bookmark from "./routes/bookmark/View.svelte"; import Bookmark from "./routes/bookmark/View.svelte";
@@ -25,6 +26,7 @@
import Messages from "./routes/message/Messages.svelte"; import Messages from "./routes/message/Messages.svelte";
import MsgSettings from "./routes/message/Settings.svelte"; import MsgSettings from "./routes/message/Settings.svelte";
import Menu from "./Components/Menu.svelte"; import Menu from "./Components/Menu.svelte";
import NewAccount from "./routes/accounting/new.svelte";
import NewPage from "./routes/wiki/AddPage.svelte"; import NewPage from "./routes/wiki/AddPage.svelte";
import Notes from "./routes/notes/Index.svelte"; import Notes from "./routes/notes/Index.svelte";
import PollList from "./routes/poll/Index.svelte"; import PollList from "./routes/poll/Index.svelte";
@@ -88,6 +90,8 @@
<span class="warn">{@html messages.warning}</span> <span class="warn">{@html messages.warning}</span>
{/if} {/if}
<Route path="/" component={User} /> <Route path="/" component={User} />
<Route path="/accounting" component={Accounts} />
<Route path="/accounting/new" component={NewAccount} />
<Route path="/bookmark" component={Bookmarks} /> <Route path="/bookmark" component={Bookmarks} />
<Route path="/bookmark/:id/view" component={Bookmark} /> <Route path="/bookmark/:id/view" component={Bookmark} />
<Route path="/calc" component={Spreadsheet} /> <Route path="/calc" component={Spreadsheet} />

View File

@@ -52,7 +52,6 @@
candidate = candidates[idx]; candidate = candidates[idx];
candidates = []; candidates = [];
selected = []; selected = [];
console.log(candidate);
onSelect(candidate); onSelect(candidate);
} }
@@ -93,6 +92,7 @@
return false; return false;
} }
candidate = { display : candidate.display };
candidates = await getCandidates(candidate.display); candidates = await getCandidates(candidate.display);
if (selected>candidates.length) selected = candidates.length; if (selected>candidates.length) selected = candidates.length;
return false; return false;
@@ -103,7 +103,7 @@
span { position : relative } span { position : relative }
select { position : absolute; top: 30px; left: 3px; } select { position : absolute; top: 30px; left: 3px; }
select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; } select { background: black; color: orange; border: 1px solid orange; border-radius: 5px; z-index: 50; }
option:checked { background: orange; color: black; } option:checked { background: orange; color: black; }
</style> </style>

View File

@@ -0,0 +1,99 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
import { api, post } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { user } from '../../user.svelte';
import Autocomplete from '../../Components/Autocomplete.svelte';
let { new_account = false } = $props();
let entry = $state({
account : {
id : 0,
name : '',
currency : ''
},
date : new Date().toISOString().substring(0, 10),
source : {
display: user.name,
user_id: user.id
},
destination : {},
amount : 0.0,
purpose : {}
});
let router = useTinyRouter();
async function save(){
let data = {
...entry,
purpose: entry.purpose.display
}
let url = api('accounting');
let res = await post(url, data);
if (res.ok) {
yikes();
router.navigate('/accounting');
} else error(res);
}
</script>
<style>
hr{
grid-column: 1 / -1;
margin: 0.5rem 0;
border: 0;
height: 1px;
align-self: center;
background: red;
}
</style>
<fieldset class="grid2">
{#if new_account}
<legend>{t('create_new_object',{object:t('account')})}</legend>
<span>{t('account name')}</span>
<input type="text" bind:value={entry.account.name} />
<span>{t('currency')}</span>
<input type="text" bind:value={entry.account.currency} />
<hr/>
{:else}
<legend>{t('add_object',{object:t('entry')})}</legend>
{/if}
<span>{t('date')}</span>
<input type="date" value={entry.date} />
<span>{t('source')}</span>
<Autocomplete bind:candidate={entry.source} />
<span>{t('destination')}</span>
<Autocomplete bind:candidate={entry.destination} />
<span>{t('amount')}</span>
<span>
<input type="number" bind:value={entry.amount} />&nbsp;{entry.account.currency}
</span>
<span>{t('purpose')}</span>
<Autocomplete bind:candidate={entry.purpose} />
<span></span>
<span>
<button onclick={save}>{t('save')}</button>
</span>
</fieldset>
<pre>
<code>
{JSON.stringify(entry,null,2)}
</code>
</pre>
<pre>
<code>
{JSON.stringify(user,null,2)}
</code>
</pre>

View File

@@ -0,0 +1,16 @@
<script>
import { useTinyRouter } from 'svelte-tiny-router';
import { t } from '../../translations.svelte';
const router = useTinyRouter();
function newAccount(){
router.navigate('/accounting/new');
}
</script>
<fieldset>
<legend>{t('accounts')}</legend>
<button onclick={newAccount}>{t('create_new_object',{'object':t('account')})}</button>
</fieldset>

View File

@@ -0,0 +1,5 @@
<script>
import EntryForm from './add_entry.svelte';
</script>
<EntryForm new_account={true} />

View File

@@ -1,5 +1,6 @@
rootProject.name = "Umbrella25" rootProject.name = "Umbrella25"
include("accounting")
include("backend") include("backend")
include("bookmark") include("bookmark")
include("bus") include("bus")
@@ -22,5 +23,4 @@ include("time")
include("translations") include("translations")
include("user") include("user")
include("web") include("web")
include("wiki") include("wiki")