diff --git a/CLAUDE.md b/CLAUDE.md
index 3572f86a..46e598c6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,4 +1,4 @@
-# CLAUDE.md — gnubok
+# CLAUDE.md — Gnubok
## Project Overview
diff --git a/app/(dashboard)/bookkeeping/[id]/page.tsx b/app/(dashboard)/bookkeeping/[id]/page.tsx
index e910d819..031a25a3 100644
--- a/app/(dashboard)/bookkeeping/[id]/page.tsx
+++ b/app/(dashboard)/bookkeeping/[id]/page.tsx
@@ -31,6 +31,7 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
const [showCorrection, setShowCorrection] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
+ const [isCommitting, setIsCommitting] = useState(false)
const [isLastInSeries, setIsLastInSeries] = useState(false)
const [attachmentCount, setAttachmentCount] = useState(0)
const [editingNotes, setEditingNotes] = useState(false)
@@ -79,6 +80,28 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
}
}, [id, toast])
+ const handleCommit = useCallback(async () => {
+ setIsCommitting(true)
+ try {
+ const res = await fetch(`/api/bookkeeping/journal-entries/${id}/commit`, { method: 'POST' })
+ const result = await res.json()
+ if (res.ok) {
+ const posted = result.data
+ toast({
+ title: 'Verifikat bokfört',
+ description: `Verifikat ${posted?.voucher_series ?? ''}${posted?.voucher_number ?? ''} har bokförts.`,
+ })
+ await fetchData()
+ } else {
+ toast({ title: 'Kunde inte bokföra', description: getErrorMessage(result, { context: 'journal_entry' }), variant: 'destructive' })
+ }
+ } catch {
+ toast({ title: 'Kunde inte bokföra verifikat', variant: 'destructive' })
+ } finally {
+ setIsCommitting(false)
+ }
+ }, [id, toast, fetchData])
+
const handleDelete = useCallback(async () => {
setIsDeleting(true)
try {
@@ -184,6 +207,18 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
{(entry.status === 'posted' || entry.status === 'draft') && (
+ {entry.status === 'draft' && (
+
+ )}
{(entry.status === 'draft' || isLastInSeries) && (
)}
{activeTab === 'vat-declaration' &&
}
+ {activeTab === 'periodisk-sammanstallning' &&
}
{isEnskildFirma && activeTab === 'ne-declaration' && (
)}
diff --git a/app/(dashboard)/settings/tax/page.tsx b/app/(dashboard)/settings/tax/page.tsx
index 2d7c3f32..bf1ce0e9 100644
--- a/app/(dashboard)/settings/tax/page.tsx
+++ b/app/(dashboard)/settings/tax/page.tsx
@@ -19,6 +19,11 @@ export default function TaxSettingsPage() {
vat_registered: vatRegistered,
vat_number: vatRegistered ? ((formData.get('vat_number') as string) || null) : null,
moms_period: vatRegistered ? ((formData.get('moms_period') as string) || null) : null,
+ periodisk_sammanstallning_period:
+ (formData.get('periodisk_sammanstallning_period') as string) || 'monthly',
+ tax_contact_name: (formData.get('tax_contact_name') as string) || null,
+ tax_contact_phone: (formData.get('tax_contact_phone') as string) || null,
+ tax_contact_email: (formData.get('tax_contact_email') as string) || null,
fiscal_year_start_month: parseInt(formData.get('fiscal_year_start_month') as string) || 1,
pays_salaries: formData.get('pays_salaries') === 'true',
preliminary_tax_monthly: parseFloat(formData.get('preliminary_tax_monthly') as string) || null,
diff --git a/app/(dashboard)/supplier-invoices/new/page.tsx b/app/(dashboard)/supplier-invoices/new/page.tsx
index e2bbba03..62432c6b 100644
--- a/app/(dashboard)/supplier-invoices/new/page.tsx
+++ b/app/(dashboard)/supplier-invoices/new/page.tsx
@@ -42,6 +42,7 @@ interface FormData {
reverse_charge: boolean
payment_reference: string
notes: string
+ paid_with_private_funds: boolean
items: LineItem[]
}
@@ -155,6 +156,7 @@ export default function NewSupplierInvoicePage() {
reverse_charge: false,
payment_reference: '',
notes: '',
+ paid_with_private_funds: false,
items: [{ description: '', amount: 0, account_number: '5010', vat_rate: 0.25 }],
},
})
@@ -165,6 +167,7 @@ export default function NewSupplierInvoicePage() {
const watchedItems = watch('items')
const watchedSupplierId = watch('supplier_id')
const watchedCurrency = watch('currency')
+ const watchedPaidPrivately = watch('paid_with_private_funds')
const isEF = entityType === 'enskild_firma'
@@ -468,11 +471,16 @@ export default function NewSupplierInvoicePage() {
function buildPayload(data: FormData) {
const vatTreatment = inferVatTreatment(data.items, data.reverse_charge)
+ // When paid privately, due_date is irrelevant — but the API still requires
+ // a YYYY-MM-DD value. Default to invoice_date so the field passes validation.
+ const dueDate = data.paid_with_private_funds && !data.due_date
+ ? data.invoice_date
+ : data.due_date
return {
supplier_id: data.supplier_id,
supplier_invoice_number: data.supplier_invoice_number,
invoice_date: data.invoice_date,
- due_date: data.due_date,
+ due_date: dueDate,
delivery_date: data.delivery_date || undefined,
currency: data.currency,
exchange_rate: data.exchange_rate ? parseFloat(data.exchange_rate) : undefined,
@@ -480,6 +488,7 @@ export default function NewSupplierInvoicePage() {
reverse_charge: data.reverse_charge,
payment_reference: data.payment_reference || undefined,
notes: data.notes || undefined,
+ paid_with_private_funds: data.paid_with_private_funds,
items: data.items.map((item) => ({
description: item.description,
amount: item.amount,
@@ -572,7 +581,10 @@ export default function NewSupplierInvoicePage() {
return
}
- if (isEF) {
+ // Privately-paid skips the AB review dialog — the toggle itself is the
+ // explicit user intent, and the resulting verifikat is just expense + VAT
+ // against the owner account (2893/2018). Same path for EF.
+ if (isEF || data.paid_with_private_funds) {
setPendingData(data)
handleDirectSubmit(data)
} else {
@@ -581,7 +593,8 @@ export default function NewSupplierInvoicePage() {
}
}
- // EF: create + auto-approve, no review dialog
+ // EF: create + auto-approve, no review dialog. Privately-paid invoices land
+ // here too and skip auto-approve since they're already in status='paid'.
async function handleDirectSubmit(data: FormData) {
setIsSubmitting(true)
await patchInboxFieldsIfChanged(data)
@@ -597,11 +610,22 @@ export default function NewSupplierInvoicePage() {
return
}
- // Auto-approve for EF
- const approveRes = await fetch(`/api/supplier-invoices/${result.data.id}/approve`, { method: 'POST' })
// Clear dirty state so useUnsavedChanges doesn't fire the
// beforeunload prompt while we navigate away on a successful submit.
reset(data)
+
+ if (data.paid_with_private_funds) {
+ toast({
+ title: 'Utlägg registrerat',
+ description: `Ankomstnummer: ${result.data.arrival_number}`,
+ })
+ router.push('/supplier-invoices')
+ setIsSubmitting(false)
+ return
+ }
+
+ // Auto-approve for EF
+ const approveRes = await fetch(`/api/supplier-invoices/${result.data.id}/approve`, { method: 'POST' })
if (!approveRes.ok) {
toast({
title: 'Varning',
@@ -856,6 +880,30 @@ export default function NewSupplierInvoicePage() {
Faktura
+ {/* Eget utlägg-toggle. När den är på bokas verifikatet direkt mot
+ skuld till ägare (2893/2018) istället för leverantörsskuld (2440),
+ och fakturan får status "Betalad" direkt. */}
+
+ (
+
+ )}
+ />
+
+
+
@@ -905,19 +953,26 @@ export default function NewSupplierInvoicePage() {
})()}
-
+
@@ -1237,17 +1292,19 @@ export default function NewSupplierInvoicePage() {
-
+ {!watchedPaidPrivately && (
+
+ )}