Jakob Wennberg 2c59c3633f feat(invoices): cross-currency settlement + payment-status card (#615)
* feat(invoices): cross-currency settlement + payment-status card

Two changes both surfaced by user feedback after PR #614:

# 1. Invoice detail page: Betalningsstatus card

The customer-invoice detail page now shows paid_amount + remaining_amount
+ the individual payment events whenever an invoice is partially_paid or
paid (was previously only a single "Paid" line on fully-paid invoices,
and nothing at all on partially_paid). Mirrors the supplier-invoice
page's payment section. Each payment row links to its verifikat.

# 2. Cross-currency match-invoice settlement

Replaces the PR #614 round-9 block (MATCH_INVOICE_CURRENCY_MISMATCH)
with proper FX-aware settlement. Flow:

1. Preview route detects tx.currency !== invoice.currency, fetches the
   Riksbanken spot rate for invoice.currency on tx.date (ML 8 kap 21–23§),
   and returns fx_conversion = { rate, rate_date, paid_in_invoice_currency }.
   When the lookup fails it returns fx_conversion.error = 'rate_unavailable'.

2. InvoiceMatchDialog renders a new Valutaomräkning card showing the rate
   + invoice-currency-equivalent + projected post-payment state + a one-
   line kursvinst/kursförlust note. When the lookup failed it swaps in
   a manual-rate input the user fills from their bank statement; the
   Confirm button blocks until a positive rate is supplied.

3. POST route does the same lookup (or accepts manual_exchange_rate from
   the request body), then:
   - paidInInvoiceCurrency = bankSek / rate (4dp precision)
   - invoice.paid_amount/remaining_amount accumulate in invoice currency
   - invoice_payments row records amount + currency = invoice.currency,
     exchange_rate = the rate actually used (not invoice.exchange_rate)
   - buildInvoicePaymentClearingLines gets paidInInvoiceCurrency so it
     credits 1510 by that × invoice.exchange_rate (booking rate) and
     posts the FX-diff line on 3960 (gain) or 7960 (loss)

4. buildInvoicePaymentClearingLines gains an optional fourth param. When
   supplied: proportional FX-aware AR-leg + balanced FX-diff. When omitted:
   pre-existing fallback (full-clear gets FX, partials defer).

The change fixes the invoice.paid_amount accumulator bug that PR #614
round-9 worked around by blocking the case entirely. Now SEK→USD
settlements actually work, with the verifikat balanced to the öre and
the GL+sub-ledger in sync per BFL 5 kap 4–5§.

Tests:
- 3 new helper tests (paidInInvoiceCurrency happy path + edge cases)
- 3 new route tests (Riksbanken happy path, lookup failure, manual rate)
- All 4321 tests pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(invoices): align cross-currency match preview with commit + review cleanups

Addresses PR #615 review feedback.

Preview/commit divergence (Greptile P1): preview/route.ts computed
paidAmount / isFullyPaid / useCashEntry from the raw SEK transaction.amount
before the FX conversion ran. A 1 000 SEK payment against a 140 USD invoice
made max(0, 140 − 1000) = 0 → is_fully_paid=true, so a cash-method unbooked
invoice previewed a cash entry (Dr 1930 / Cr 30xx) while the POST handler —
which converts first — commits the clearing entry (Dr 1930 / Cr 1510). The
user approved one verifikat and a different one was booked. Move the FX
lookup above the paid/remaining math so paidAmount derives from the
invoice-currency conversion, mirroring the POST handler. Rate-unavailable
stays non-fully-paid so the cash shape is never previewed on a guess.

Add a preview-route regression test (cross-currency → clearing + not
fully paid; same-currency cash path still previews the cash entry).

Cleanups:
- Bound manual_exchange_rate with .max(100000) as a sanity ceiling against
  pasted/garbage input corrupting the FX-diff posting (swarm V2.3).
- Remove the invisible disabled placeholder retry button and its unused
  fx_manual_rate_retry i18n keys (Greptile P2).
- Remove the now-unreachable MATCH_INVOICE_CURRENCY_MISMATCH error code
  (Greptile P2 dead code; confirmed zero references).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(invoices): record FX rate provenance + cover kursförlust path

Follow-up to the PR #615 review (compliance swarm V16 / SOC 2 CC6.1 /
GDPR Art.5(1)(f); Swedish accounting review).

A manually-supplied cross-currency rate is a user-controlled money-path
override of the ML 8 kap 21–23§ obligation and was indistinguishable from
an automatic Riksbanken lookup in the audit trail. Tag the resolved rate
with source: 'manual' | 'riksbanken' and:
- write a "Manuell valutakurs <rate> <ccy>/SEK (betalningsdatum …)" note
  onto the existing invoice_payments.notes column when manual (BFL 5 kap
  6–7§ — the verifikation must reflect the actual affärshändelse);
- record rate_source + exchange_rate in payment_match_log.new_state.
No schema change — both are existing columns/JSON.

Tests:
- cover the kursförlust (7960 Dr) branch of the cross-currency
  paidInInvoiceCurrency path — previously only the 3960 gain was asserted;
- assert rate_source provenance ('manual' and 'riksbanken') reaches the
  match-log new_state on both FX paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 10:45:51 +02:00
2026-05-28 15:43:48 +02:00
2026-05-28 15:43:48 +02:00
2026-05-06 11:12:02 +02:00
2026-04-22 15:33:04 +02:00
2026-05-28 15:43:48 +02:00
2026-05-22 12:22:15 +02:00

gnubok

Open-source Swedish accounting software for sole traders (enskild firma) and limited companies (aktiebolag).

License: AGPL-3.0-or-later

What is gnubok?

gnubok implements double-entry bookkeeping compliant with Swedish accounting law (Bokforingslagen). It supports the BAS 2026 chart of accounts, handles VAT declarations (momsdeklaration), SIE import/export, and enforces 7-year document retention. Built for sole traders and limited companies operating in Sweden.

Features

  • Double-entry bookkeeping -- BAS 2026 chart of accounts, draft/commit workflow, sequential voucher numbering
  • Invoicing -- Create, send, and track invoices with mixed VAT rates and PDF generation
  • Bank reconciliation -- PSD2 bank connection via Enable Banking, 4-pass automatic matching
  • VAT declaration -- SKV 4700 form mapping, per-rate breakdown, EU/export handling
  • Tax reports -- NE-bilaga, INK2, SRU export for Skatteverket
  • Supplier invoices -- Registration, payment tracking, input VAT deduction
  • Document archive -- SHA-256 integrity, 7-year retention enforcement, full archive ZIP export
  • SIE import/export -- Standard Swedish accounting interchange format
  • Extension system -- Opt-in plugins for AI categorization, receipt OCR, email, calendar, and more

Self-Hosting

git clone https://github.com/erp-mafia/gnubok.git
cd gnubok
./setup.sh              # Prompts for Supabase credentials, generates .env
docker compose up -d

You need a Supabase project and must apply the database migrations before first use. See SELF-HOSTING.md for the full step-by-step guide, including Supabase setup, auth configuration, optional features (AI, email, push notifications), and troubleshooting.

Development Setup

Prerequisites: Node.js 20+, a Supabase project.

npm install
npm run dev       # Start dev server (auto-generates extension registry)
npm test          # Run tests
npm run build     # Production build
npm run lint      # ESLint

Tech Stack

  • Framework: Next.js 16 (App Router), React 19, TypeScript (strict)
  • Database: Supabase (PostgreSQL + Row Level Security + email/password auth + TOTP MFA)
  • Styling: Tailwind CSS 4 + shadcn/ui
  • Integrations: Enable Banking (PSD2), Anthropic SDK, LangChain, OpenAI, Resend, JSZip

Documentation

  • SELF-HOSTING.md -- Full self-hosting guide (Docker, Supabase setup, migrations, optional features)
  • CLAUDE.md -- Architecture, bookkeeping engine, database conventions, extension system
  • CONTRIBUTING.md -- Development workflow, code style, pull request process
  • SECURITY.md -- Vulnerability reporting policy

Contributing

Contributions are welcome. See CONTRIBUTING.md for the full guide.

All commits require a DCO sign-off (git commit -s).

License

AGPL-3.0-or-later with an extension exception: third-party extensions that interact solely through the documented Extension API may be licensed under any terms, including proprietary. See LICENSE for details and NOTICE for third-party attributions.

S
Description
Accounted — svensk bokföringsmotor (AGPL, BAS 2026, BFL-compliant, 150+ MCP tools). Finance-kapacitet brevet ERPNext. ADR-ENGAGEMENT-001-tillägg.
Readme AGPL-3.0 55 MiB
Languages
TypeScript 93%
PLpgSQL 5.8%
JavaScript 0.4%
HTML 0.3%
MDX 0.2%
Other 0.1%