ExpenseVibe

Logo

Utlägg, kilometerersättning och frånvaro med export till Visma Lön 600

View the Project on GitHub montegrotto/ExpenseVibe

Installation och drift

Den här sidan beskriver installation, konfiguration och drift av ExpenseVibe. Hur man använder systemet står i användarguiden. Källkoden finns på GitHub.

Bilagor tolkas (best effort) av systerprojektet ReInvGrabber via dess /process-one-endpoint - se attachments/extraction_client.py. Integrationen är frivillig och avstängd tills REINVGRABBER_URL sätts. Bilagehantering och visuellt uttryck är medvetet byggt likt SaldoVibe.

Förutsättningar

Installation

python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
npm install

Körning

cp .env.example .env   # sätter DJANGO_DEBUG=1 lokalt, annars krävs collectstatic
source .venv/bin/activate
python manage.py migrate
python manage.py runserver

Öppna sedan http://127.0.0.1:8000 - så länge systemet inte har en enda användare visas en länk på inloggningssidan för att skapa det första kontot, som automatiskt blir administratör (inget CLI-kommando krävs). Så fort det kontot finns stängs den vägen permanent - alla efterföljande användare skapas av en administratör via adminmenyn. Skapa fler användare, kategorier och en km-sats via adminmenyn innan du testar hela flödet.

Sätt miljövariabeln REINVGRABBER_URL (t.ex. http://127.0.0.1:5001) för att slå på automatisk tolkning av uppladdade kvitton - tom/osatt betyder att integrationen är avstängd och uppladdning fungerar precis ändå, bara utan förifyllda fält.

Miljövariabler

Variabel Standard Beskrivning
DJANGO_SECRET_KEY osäker dev-nyckel, bara med DJANGO_DEBUG=1 Utan DEBUG vägrar appen starta om den saknas.
DJANGO_DEBUG av 1 lokalt, annars krävs collectstatic.
EXPENSEVIBE_PUBLIC_URL – Publik adress, t.ex. https://expensevibe.example.se. Ger ALLOWED_HOSTS och CSRF_TRUSTED_ORIGINS om de inte sätts explicit.
DJANGO_ALLOWED_HOSTS – Kommaseparerad. localhost/127.0.0.1/[::1] läggs alltid till.
DJANGO_CSRF_TRUSTED_ORIGINS – Kommaseparerad.
DATABASE_ENGINE, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, DATABASE_HOST, DATABASE_PORT SQLite Sätt DATABASE_ENGINE=django.db.backends.postgresql för Postgres.
EXPENSEVIBE_DATA_DIR repo-roten Var SQLite-databas och media hamnar.
EXPENSEVIBE_MAX_UPLOAD_BYTES 10 MB Uppladdningsgräns i minnet. nginx har egen gräns (client_max_body_size).
REINVGRABBER_URL tom (av) ReInvGrabber-tjänstens adress.
REINVGRABBER_TIMEOUT 15 Sekunder innan tolkningen ges upp.

Tester

python manage.py test

Lint

ruff check .

iPhone-app

SwiftUI-app för det man gör på språng: lägga upp utlägg och kilometerrapporter samt attestera dem. Den pratar med JSON-API:t under /api/ på samma server - se ios/README.md.

Docker

Färdigbyggda imager (amd64 och arm64) publiceras på Docker Hub som montegrotto/expensevibe vid varje release. docker-compose.yml kör den med PostgreSQL, nginx och ReInvGrabber i samma nätverk. Utveckling sker utan Docker, se Körning ovan.

cp .env.example .env   # avkommentera produktionsraderna och fyll i riktiga värden
docker compose up -d

docker compose up hämtar imagen från Docker Hub. Sätt EXPENSEVIBE_VERSION i .env för att låsa en viss version, annars körs latest; uppgradera med docker compose pull && docker compose up -d. docker compose up --build -d bygger imagen lokalt från Dockerfile (Python, Node.js, WhiteNoise, Gunicorn) under samma namn i stället för att hämta den.

Compose läser .env automatiskt, både för ${NGINX_HTTP_PORT} i själva YAML-filen och (via env_file) som miljö i containrarna. Ändra NGINX_HTTP_PORT där (t.ex. till 8080) om port 80 redan är upptagen på din maskin.

Migrationer körs automatiskt vid uppstart av web-tjänsten, se scripts/docker-entrypoint.sh.

nginx-konfigen ligger inline i docker-compose.yml (configs:), så en driftsättning behöver bara den filen och .env. Bakom en egen reverse proxy (t.ex. nginx-proxy-manager) på samma värd: lägg en compose.override.yml bredvid som tar bort den publicerade porten och ansluter nginx till proxyns nätverk, så når proxyn stacken på namnet expensevibe-nginx:

services:
  nginx:
    ports: !reset []
    networks:
      default:
      proxy:
        aliases:
          - expensevibe-nginx

networks:
  proxy:
    external: true

Proxyn ska skicka X-Forwarded-Proto: https; nginx för det vidare till Django som då bygger https-länkar.

Frånvarorapportering

Användarflödet (kalender, tidrapport, perioder, arbetsscheman) beskrivs i användarguiden. Här bara det som rör drift och kod.

Varje anställds veckoschema (absence.models.WorkSchedule) styr både omräkningen av Hel dag-frånvaro till timmar, taket för summan av timbaserade poster samma dag och Visma-exportens NormalWorkingTime-block (se nedan). Hel dag-poster sparar timmarna från schemat vid varje sparning (AbsenceEntry.workday_hours_snapshot); inlämnade poster sparas aldrig om och påverkas därför inte av senare schemaändringar.

Redovisning sker per kalendermånad (absence.models.Period) - saknas en Period-rad för en månad är den stängd för rapportering. Perioder öppnas normalt av open_period-kommandot, tänkt att köras via extern cron den 1:a varje månad:

0 0 1 * * docker compose exec -T web python manage.py open_period

Kommandot är idempotent (ofarligt att köra flera gånger) och kan även öppna en annan månad manuellt: python manage.py open_period --month 2026-04. Adminsidan “Perioder” har samma funktion som fallback om cron missas.

Svenska helgdagar (inklusive påsk-baserade som beräknas om varje år) räknas ut av absence/holidays.py, listas aldrig manuellt och kräver inget admin-underhåll. Julafton, nyårsafton och midsommarafton räknas som arbetsfria både i kalendern och i exportens normalarbetstid.

Export till Visma Lön 600

Exporten bygger en .tlu-fil (Vismas tidsredovisningsformat, XML, ISO-8859-1) enligt Vismas egen specifikation - se export/services.py. Löneart-/tidkoder (Category.code / MileageRate.export_code / AbsenceType.export_code) måste motsvara koder som redan är konfigurerade i Visma Lön 600 - ExpenseVibe definierar inga nya koder där. Gör en verklig testimport i Visma Lön 600 innan skarp drift.

Utlägg och kilometerrapporter väljs radvis inom ett fritt datumintervall men summeras i filen till en RegOutlay per anställd och löneart, daterad “Period till” (eller personens sista postdatum om fältet är tomt) - se _merged_regoutlays. Frånvaro väljs periodvis istället (kryssrutor per period under “Frånvaro” i exportvyn) - att välja en period exporterar alla dess attesterade tidrapporter i samma .tlu-fil som utläggen/kilometer- rapporterna, och markerar perioden “rapporterad” om inga oattesterade tidrapporter återstår för den. Om någon anställds tidrapport ännu inte är attesterad när perioden exporteras förblir perioden öppen tills den är det, så en tidig export inte permanent stänger ute sena rapporter. En period som ändå behöver rättas efter att den blivit “rapporterad” (t.ex. en sent avvisad tidrapport) kan återöppnas manuellt under “Perioder”.

Frånvaro i filen kräver två extra element utöver RegOutlay/Time, båda upptäckta genom verkliga testimporter i Visma (inte dokumenterade som “obligatoriska” på ett sätt som var uppenbart i förväg):

Demodata

scripts/seed_demo.py fyller en instans (t.ex. demo.expensevibe.se) med fyra påhittade användare - administratör, attestant och två anställda - med utlägg, kilometerrapporter, frånvaro och tidrapporter i alla statusar, kvitton i inkorgen och en körd Visma-export. Befintliga konton och data rörs inte, och skriptet vägrar köra om demoanvändarna redan finns. Alla fyra konton (admin@, attestant@, erik@ och [email protected]) får lösenordet i DEMO_PASSWORD; utan det slumpas ett som skrivs ut.

docker compose exec -T -e DEMO_PASSWORD='...' \
    web gosu appuser /opt/venv/bin/python /app/manage.py shell < scripts/seed_demo.py

Datumen räknas bakåt från i dag, så kör skriptet när instansen sätts upp. Med ReInvGrabber påslaget tar körningen någon minut eftersom varje kvitto tolkas vid uppladdningen.

Kända begränsningar (v1)