druvu-acc

Read and write GnuCash books from Java. Accounts, transactions, commodities and prices; the business side too, customers, invoices, bills, lots. What you save opens in GnuCash.

A companion to GnuCash, not a replacement. It reads what GnuCash wrote, writes what GnuCash will read, and leaves untouched whatever it does not model.

View on GitHub Maven Central

Not a programmer? Give your AI assistant the address of the page written for it, https://github.com/DenissLarka/druvu-acc-parent/blob/main/docs/ai-skill.md, and say what you want in plain words. See it done in five minutes.


Install

On Maven Central from version 2.2.0 — use 2.2.0 or newer. No extra repository, no credentials. One dependency is enough for GnuCash files; it brings the API with it:

<dependency>
    <groupId>com.druvu</groupId>
    <artifactId>druvu-acc-gnucash-xml</artifactId>
    <version>2.2.1</version>
</dependency>

No project at all? A single file runs with JBang, which fetches Java 25 and the library by itself — put //DEPS com.druvu:druvu-acc-gnucash-xml:2.2.1 at the top of the file and run jbang YourFile.java. Three complete, runnable accounting stories — a household’s books, a contractor’s invoice with VAT, the customer behind a payment — are in the docs folder.



Reading

// Load a GnuCash file (auto-discovers implementation via ServiceLoader)
AccStore store = AccStore.load(Path.of("/path/to/book.gnucash"));

// Iterate over accounts
for (Account account : store.accounts()) {
    System.out.println(account.name() + " [" + account.type() + "]");
}

// Get transactions
for (Transaction tx : store.transactions()) {
    System.out.println(tx.description() + " - " + tx.datePosted());
    for (Split split : tx.splits()) {
        System.out.println("  " + split.accountId() + ": " + split.value());
    }
}


Writing

Load a store as a WritableAccStore to add or remove entities and save the result. Output is GnuCash-compatible (opens directly in GnuCash).

WritableAccStore store = AccStore.loadWritable(Path.of("/path/to/book.gnucash"));
String rootId = store.rootAccounts().getFirst().id();
CommodityId chf = CommodityId.CHF;

// Two accounts; store.newId() mints an id in the format the file expects
String coffeeId = store.newId();
store.addAccount(Account.of(coffeeId, "Coffee", AccountType.EXPENSE)
        .withCommodity(chf)
        .withParent(rootId));

String cashId = store.newId();
store.addAccount(Account.of(cashId, "Cash", AccountType.CASH)
        .withCommodity(chf)
        .withParent(rootId));

// A balanced transaction between them - the splits' values must sum to zero
String txId = store.newId();
LocalDate today = LocalDate.now();
BigDecimal price = new BigDecimal("4.50");
store.addTransaction(Transaction.of(txId, chf, today, "Morning coffee", List.of(
        Split.of(store.newId(), txId, coffeeId, today, price),
        Split.of(store.newId(), txId, cashId, today, price.negate()))));

// Track an investment: define a security and record a price quote
store.addCommodity(Commodity.security("NASDAQ", "AAPL", "Apple Inc.", 10000));
store.addPrice(new Price(store.newId(), CommodityId.security("NASDAQ", "AAPL"), CommodityId.USD,
        today.atStartOfDay(), "user:price-editor", Optional.of("last"), new BigDecimal("212.50")));

// Persist (gzip-compressed when the path ends with .gnucash or .gz)
store.save(Path.of("/path/to/book-modified.gnucash"));

A book can also start from nothing — AccStore.newBook(CommodityId.CHF) — and everything is in three packages: com.druvu.acc.api, com.druvu.acc.api.entity, com.druvu.acc.api.service.



What you get

  • Balances that add up honestly. AccountService totals an account and its subtree as a MultiAmount, one quantity per commodity, so multi-currency and investment subtrees never pretend to be one currency. A single quantity is a typed Amount: 1500.00 CHF, or 100 NASDAQ/AAPL shares.
  • Tolerant on read, strict on write. A damaged book can still be inspected or repaired. validate() reports structural problems, a second root, a dangling parent, a split on a missing account, and save refuses a book that fails them.
  • Nothing lost on the way through. An update writes only the fields the library understands onto the record already in the file. GnuCash’s own extensions, custom key-value slots and the out-of-scope entities survive a load-modify-save unchanged.
  • A book from nothing. AccStore.newBook(CommodityId.CHF). Everything is in three packages: com.druvu.acc.api, com.druvu.acc.api.entity, com.druvu.acc.api.service.

What is covered, and what is not

The core double-entry entities, the investment and multi-currency entities and the business side are read and written. Scheduled transactions and budgets are GnuCash’s planning tools, not bookkeeping: out of scope, and left exactly as GnuCash wrote them.

Entity In GnuCash druvu-acc
Accounts ✓ read + write
Transactions & splits ✓ read + write
Commodities (currencies & securities) ✓ read + write
Prices (price database) ✓ read + write
Account flags (placeholder, hidden, notes, colour) ✓ read + write
Scheduled (recurring) transactions ✓ out of scope — preserved on save
Budgets ✓ out of scope — preserved on save
Customers / Vendors / Employees ✓ read + write
Invoices & bills (+ line entries) ✓ read + write
Jobs / Orders ✓ read + write
Billing terms / Tax tables ✓ read + write
Lots ✓ read + write

Posting an invoice and applying a payment stay GnuCash’s own job: the library reads what a posted document leaves behind (including the customer behind any ledger transaction) but does not post.


Java 25 or newer; a JBang script fetches it by itself.


Something it almost does?

Open an issue describing your use case, or write to me. What gets built next is decided by whoever turns up and asks.

Apache License 2.0.


Back to top

Copyright © 2026 druvu.com. All rights reserved.

This site uses Just the Docs, a documentation theme for Jekyll.