Jakob Wennberg df29817826 fix(supplier-invoices): make the 'overdue' label two-way and stop it locking an invoice (#1227)
* fix(supplier-invoices): make the 'overdue' label two-way and stop it locking an invoice

The daily cron flips unbooked payables past their due date to 'overdue' but
nothing ever flipped them back, so aging alone pushed an invoice out of every
workflow that gated on 'registered': it could not be edited (not even to extend
the due date that made it overdue) and it could not be attested. Deletion was
already unblocked in #1204; this closes the rest of #1206.

- update_overdue_supplier_invoices() gains the inverse branch: a payable whose
  due date is no longer in the past returns to its resting status. Because the
  flip collapses 'registered' and 'approved', the un-flip needs a separate
  attest marker: new supplier_invoices.approved_at, backfilled from updated_at
  for rows currently sitting in 'approved'.
- PUT /api/supplier-invoices/[id] accepts every unsettled status and recomputes
  the label from the due date it writes, in both directions, instead of leaving
  it up to a day stale. The update body carries metadata only (numbers, dates,
  reference, notes), never amounts or accounts, so a posted registration
  verifikat cannot be desynced by money.
- Approve (web route, v1 API, MCP staging tool, staged commit executor) keys off
  approved_at instead of status === 'registered', so an aged invoice can still
  be attested. A still-late invoice keeps the 'overdue' label after attest:
  approving is not a reason to hide that the money is late.
- One shared predicate in lib/supplier-invoices/lifecycle.ts for all five call
  sites, mirroring the SQL; new SI_EDIT_INVALID_STATUS replaces the raw Swedish
  string the edit gate used to return.

Tests: 12 pg-real cases on the cron (5 new, covering both directions and the
credit-note/fully-paid boundaries), plus route tests asserting the exact written
payload for PUT and approve, and unit tests pinning the shared predicate against
the SQL. npm test (11385), lint, check:guards clean.

Closes #1206

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

* docs(migration): mark backfilled approved_at values as derived, not audit facts

Compliance review on #1227 flagged that approved_at = updated_at could later be
mistaken for an observed attestation moment (BFNAR 2013:2 kap 8
behandlingshistorik). The column comment and the migration now state plainly
that pre-migration values are derived and that audit_log, written by the
audit_supplier_invoices trigger, remains the record of what happened.

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

* fix(supplier-invoices): guard the derived status writes with compare-and-swap

Review findings on #1227. The status these paths write is derived from facts
read a moment earlier, so an unconditional write could overwrite a concurrent
cron flip, edit or approval with a label computed from what those changed.

- PUT pins status, due_date and approved_at when (and only when) it derives a
  new status; zero matched rows is now a retryable 409 SI_EDIT_CONFLICT instead
  of a silently stale label. Metadata-only updates keep writing unconditionally:
  they never touch status, so they cannot clobber it.
- The web approve route and the staged-commit executor gain the same
  pre-approval guard the v1 route already had (status in registered/overdue,
  approved_at IS NULL) plus a !data race check, so two concurrent approvals can
  no longer both stamp approved_at and both emit supplier_invoice.approved.
- The v1 guard additionally pins due_date, since nextStatus is derived from it.
- The list page no longer invents status/approved_at when the approve response
  is incomplete: it re-reads instead. An operator about to pay must not be shown
  a fabricated lifecycle state.
- route.overdue.test.ts clears the module-level event bus like its sibling.

Tests: new conflict cases for both paths (409 on PUT, refusal without an event
emission on approve). npm test 11387 passed, lint 0 errors, check:guards clean,
12 pg-real cases green.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:49:22 +02:00
2026-07-27 03:54:42 +02:00
2026-07-21 23:00:15 +02:00
2026-07-24 15:03:50 +02:00
2026-06-12 16:35:30 +02:00
2026-07-27 03:54:42 +02:00
2026-07-25 22:56:17 +02:00
2026-07-21 23:00:15 +02:00
2026-07-24 15:03:50 +02:00
2026-07-21 23:00:15 +02:00
2026-07-05 03:05:09 +02:00
2026-07-08 09:54:46 +02:00
2026-07-24 15:03:50 +02:00
2026-07-24 15:03:50 +02:00
2026-07-23 16:16:55 +02:00
2026-07-12 19:14:12 +02:00
2026-07-05 03:05:09 +02:00
2026-07-05 03:05:09 +02:00
2026-07-25 22:56:17 +02:00

Accounted

Open-source Swedish accounting software for sole traders (enskild firma) and limited companies (aktiebolag). Double-entry bookkeeping that complies with Swedish accounting law, built to be operated by you or by your AI agent.

License: AGPL-3.0-or-later Core Build pg-real tests Docker

Website · Hosted app · Documentation

Why Accounted?

Compliant by construction. Accounted implements double-entry bookkeeping under Swedish accounting law (Bokföringslagen). Voucher immutability, sequential voucher numbering, period locks, and 7-year document retention are enforced by database triggers, not by convention. Corrections are made the legal way, with reversal entries (storno), never by editing history. See ARCHITECTURE.md for how.

Agent-native. The full bookkeeping engine is exposed as 100+ MCP (Model Context Protocol) tools with scoped API keys, so an AI agent can do the books in Accounted: categorize transactions, draft vouchers, reconcile periods, and prepare declarations. Posting is staged for human approval, so the agent proposes and you decide.

Yours to run. AGPL-3.0 licensed and fully self-hostable with Docker and Supabase. Use the hosted version at app.gnubok.se or run your own.

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
  • Payroll -- Salary runs, payslips, and AGI (arbetsgivardeklaration) employer declarations
  • 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
  • Agent access (MCP) -- 100+ bookkeeping tools over the Model Context Protocol, with scoped API keys and staged approvals
  • Extension system -- Opt-in plugins for AI categorization, receipt OCR, email, calendar, and more

Self-Hosting

git clone https://github.com/erp-mafia/accounted.git
cd accounted
./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 docs/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

See CONTRIBUTING.md for the full development workflow.

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

Community

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%