Local-first bookkeeping, VAT (moms) and invoicing for a Swedish sole proprietorship (enskild firma). Everything runs on your own machine — no cloud, no external APIs, no telemetry. Data lives in a local SQLite database plus an uploads folder.
⚠️ Scope: Firmabok is bookkeeping support material (underlag). It computes moms according to Swedish rules and maps the figures to the momsdeklaration (SKV 4700) boxes, but you review and file everything in Skatteverket's e-services. Income tax is intentionally not calculated; the NE-bilaga export is support material only.
Moms is calculated on taxable sales, never on profit:
| Concept | Definition in Firmabok |
|---|---|
| Gross income (brutto) | Sales incl. VAT |
| Net income (nettoomsättning) | Sales excl. VAT |
| Output VAT (utgående moms) | VAT on sales at 25/12/6 % |
| Input VAT (ingående moms) | Deductible VAT on purchases |
| VAT payable/refund (fält 49) | Output − Input |
| Expense cost | Gross − deductible VAT (non-deductible VAT is a cost) |
| Profit (resultat) | Net income − expense costs |
| Egna uttag/insättningar | Tracked separately — never income/expense, no VAT effect |
A "simplified mode" exists for comparison only. It is disabled by
default, requires typing JAG FÖRSTÅR to enable, and is clearly labelled
EJ KORREKT / not for Skatteverket wherever its numbers appear.
- FastAPI + Uvicorn — modern Python web framework with first-class Pydantic validation and a simple dependency system; Flask would work but FastAPI gives typed forms/validation for free. Server-rendered pages + light HTMX, styled with Pico.css (both vendored locally — no Node/npm, no CDN).
- SQLite via SQLAlchemy 2.0 + Alembic migrations. Money is
stored as exact decimal text through a custom
MoneyType— SQLite never sees floats. All arithmetic usesDecimalwith half-up rounding to öre. - Jinja2 + WeasyPrint for PDF invoices/reports (HTML+CSS → PDF).
- Local auth: scrypt-hashed password, server-side sessions (HttpOnly cookie), CSRF tokens on every mutating request, audit trail of all changes to financial data.
app/
main.py FastAPI app factory + startup migrations/seeding
models.py SQLAlchemy data model (17 tables)
money.py Decimal helpers, Swedish formatting (1 234,56 kr)
swedish.py org.nr/VAT-nr validation, OCR (LUHN), fiscal periods
vat.py VAT codes + momsdeklaration box engine (SKV 4700)
invoices.py numbering, totals, finalize, credit notes
reports.py monthly/yearly totals, P&L, CSV exports
pdf.py WeasyPrint rendering
backup.py backup/restore CLI (also: firma backup)
security.py auth, sessions, CSRF
audit.py audit trail
routers/ one module per UI area
templates/ Jinja2 pages + PDF templates
migrations/ Alembic (0001 = full initial schema)
tests/ 65+ tests: VAT math, boxes, rounding, numbering, web flows
docs/ daily use, Skatteverket reporting, backup
Requirements: NixOS with flakes enabled (or use shell.nix with
nix-shell). WeasyPrint's native dependencies (pango, cairo, gdk-pixbuf,
harfbuzz, fontconfig, glib, libffi, zlib, libjpeg, openjpeg, freetype,
libxml2, libxslt) and fonts (DejaVu, Liberation) come from the flake.
cd firma
nix develop # enters shell with python312 + uv + all native libs
uv sync # installs pinned Python deps (uv.lock)
uv run uvicorn app.main:app --reload
# open http://127.0.0.1:8000 → first-run wizard creates your local loginYour company profile is already configured from your Skatteverket documents (Registerutdrag SKV 4621 + Beslut debiterad preliminärskatt) by running:
uv run python scripts/set_company_profile.py # already applied to data/firma.dbApplied settings: Saddam Hussain, org.nr 830116-0571, momsreg.nr SE830116057101, Godkänd för F-skatt (fr.o.m. 2026-09-14), momsregistrerad fr.o.m. 2026-09-15, kvartalsvis momsredovisning med bokslutsmetoden (kontantmetoden), kalenderår, betalningsvillkor 10 dagar (enligt din fakturamall). Bankuppgifter saknas medvetet — Bankgiron på samplefakturan tillhör ett annat företag (Nordic Taxi och Gods Transport AB); fyll i dina egna under Inställningar. Telefon/e-post fanns inte i dokumenten.
Demo data (separate throwaway profile) can still be tried with:
uv run python -m app.seednix profile install . # from the project directory
firmabok init # create/migrate DB in ~/.local/share/firmabok
firmabok serve # http://127.0.0.1:8000
firmabok backup # timestamped backupIf the PDF button reports missing pango/cairo/gobject libraries, your server
process lacks LD_LIBRARY_PATH. Bullet-proof starter (works in any shell,
with or without nix-shell, activates .venv automatically):
scripts/run.sh # computes LD_LIBRARY_PATH from nixpkgs, starts uvicornThe app also self-heals: if WeasyPrint fails to load its libs on NixOS, it
computes the path via nix-instantiate and re-executes itself once.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# WeasyPrint needs system libs: pango cairo gdk-pixbuf harfbuzz fontconfig …
# (Debian/Ubuntu: sudo apt install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0)
uvicorn app.main:app --reloadTroubleshooting: if
uv syncfails withTOML parse error … missing field 'distribution', youruvis too old (nixos-stable channels ship an ancient one). Fix: usenix develop(the flake provides a modern uv), or skip uv:python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt, or let the old uv re-resolve:rm uv.lock && uv sync.
- Open
http://127.0.0.1:8000→ create your local user (first run). - Inställningar → fill in company profile: namn, org.nr (
XXXXXX-XXXX, LUHN-validated), momsreg.nr (SE+ org.nr +01, validated), F-skatt, address, bank (Bankgiro/Plusgiro/IBAN/BIC), logo, invoice defaults. - Inställningar → Bokföring: fiscal year (calendar by default, brutet år supported), momsperiod (kvartalsvis default, also månadsvis/årsvis), standard VAT rate.
- Register income under Intäkter (per week/month — date driven), expenses under Utgifter (receipt upload supported).
- Create invoices under Fakturor: draft → review → Fastställ
(assigns the next sequential number
ÅÅÅÅ-NNNN, due date, OCR) → download PDF. Corrections: kreditfaktura (never delete/edit a finalized invoice — gapless numbering is a legal requirement). - Moms: pick period → see declaration boxes (fält 05–62) → export CSV/PDF → Spara & lås when filed.
- Rapporter: P&L, monthly grid, category breakdown; CSV exports for bokföringsorder and NE-bilaga support material.
- Ägare: record egna uttag/insättningar (kept out of P&L and VAT).
- Data: one-click backup (DB + uploads + sha256 manifest), restore, JSON export, audit log.
- Series per fiscal year:
2026-0001,2026-0002, … (prefix/digits/start configurable). - Numbers are consumed only when an invoice is finalized → drafts can be deleted without creating gaps.
- Finalized invoices are immutable and undeletable; fix mistakes with a credit note (gets its own number, negative lines, references original, marks the original "Krediterad").
- OCR = invoice-number digits + LUHN check digit ( Swedish bank standard).
uv run pytest # 65+ testsCovers: öre-exact VAT math and half-up rounding, the "VAT is never 25 % of profit" regression, all momsdeklaration box mappings (incl. reverse charge, imports, exempt/EU sales), box 49 whole-kronor rule, gapless numbering, credit notes, P&L (owner transactions excluded), CSV exports, auth/CSRF, and full web flows.
uv run python -m app.backup backup # or scripts/backup.sh (cron-friendly)
uv run python -m app.backup list
uv run python -m app.backup restore <backup-name> # stop the app firstBackups are plain directories in data/backups/ containing a consistent
SQLite copy (online backup API, WAL-safe), the uploads folder and a
manifest.json with a sha256 checksum. Restoring automatically takes a
safety copy of the current state first. See docs/backup_restore.md.
- Bokslutsmetoden (kontantmetoden) is implemented per your SKV registration: output VAT is reported when the customer has paid (payment date), and unpaid items are swept into the fiscal year's last period at the latest. Input VAT defaults to the simplification rule (deduct when booked, turnover < 1 Mkr); a setting switches it to payment-date basis. Remember to register payment dates on income!
- Momsdeklaration boxes follow Skatteverket's current form (SKV 4700): sales boxes 05–08, output VAT 10–12, reverse-charge purchases 20–24 with output VAT 30–32, exempt/EU sales 35–42, input VAT 48, import 50 with 60–62, and box 49 = (10+11+12+30+31+32+60+61+62) − 48 in whole kronor (ML 1994:200 1 kap. 7 § — the ledger keeps öre precision).
- Reverse charge sales (byggtjänster, EU B2B services/goods) are supported per invoice line: text "Omvänd skattskyldighet" on the PDF, no VAT charged, correct box mapping (41/39/35/36/40).
- Non-deductible VAT (e.g. representation above the deduction limit) is handled by the per-expense "avdragsgill moms" field — the remainder becomes part of the cost, as it should for an enskild firma.
- Swedish formatting everywhere:
YYYY-MM-DD,1 234,56 kr.