Utlägg, kilometerersättning och frånvaro med export till Visma Lön 600
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.
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
npm install
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.
| 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. |
python manage.py test
ruff check .
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.
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.
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.
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):
<TimeCodes> - en katalog (syskon till SalaryDataEmployee) som listar
Code/TimeCodeName för varje AbsenceType.export_code som faktiskt
förekommer i exporten. TimeCodes är obligatoriskt; saknas det ger
Visma “Importen av tidkoder misslyckades”. Motsvarande katalog för
utlägg, RegOutlayCodes, skrivs också ut (en rad per OutlayCode som
förekommer) - det är den Visma uppdaterar sitt utläggsregister från, så
utan den hanteras egentillagda utläggskoder inte korrekt vid importen.<NormalWorkingTimes> per anställd som har frånvaro med i exporten - en
rad per kalenderdag i personens period, med timmar hämtade från
personens Arbetsschema (se ovan). Saknas det, eller stämmer det inte
mot personens schema i Visma Lön, ger Visma “Normalarbetstid saknas
eller har fel format i filen”.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.
Expense.currency sparas men exporten skickar
amount rakt av till Visma som kronor./konto/byt-losenord/.