feat(invoices): offer proforma where the invoice is created (#2217) (#2254)

Proforma existed (document_type 'proforma', its own conversion path and
views) but the only way to pick it was the collapsed Förval panel inside
the editor. A user coming from Fortnox looked for it next to "Ny faktura",
did not find it, and concluded the feature was missing.

"Ny proformafaktura" is now an entry in the create split button on
/invoices, driven by ?proforma=1 exactly like ?quote=1 drives "Ny
offert": the URL opens NewInvoiceDialog with the proforma type
preselected, the dialog's accessible title says so, and closing the
dialog clears the param. Keys added to sv and en.

Closes #2217


Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 18:56:14 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent c3b7e79af8
commit 2c3ffaf51c
4 changed files with 33 additions and 4 deletions
+25 -2
View File
@@ -47,6 +47,7 @@ import {
FileInput,
FileDown,
FileText,
FileClock,
} from 'lucide-react'
import { StartCard } from '@/components/dashboard/StartCard'
import { useCompany } from '@/contexts/CompanyContext'
@@ -246,8 +247,12 @@ export default function InvoicesPage() {
const copyFromId = searchParams.get('copy')
const showNewInvoice = searchParams.has('new') || copyFromId !== null
const openSelfBilled = searchParams.has('self')
// ?quote=1 preselects the offert document type ("Ny offert" split entry).
// ?quote=1 preselects the offert document type ("Ny offert" split entry);
// ?proforma=1 the proforma type ("Ny proformafaktura" split entry, #2217:
// proforma existed but only behind the collapsed Förval panel inside the
// editor, so a user coming from Fortnox concluded it did not exist).
const openQuote = searchParams.has('quote')
const openProforma = searchParams.has('proforma')
const showRotRutPayout = searchParams.has('rot-rut')
// Open/close handlers rewrite only their own keys: a hardcoded '/invoices'
// would destroy the ?status= view write-back (and any other params) every
@@ -264,6 +269,7 @@ export default function InvoicesPage() {
p.delete('new')
p.delete('self')
p.delete('quote')
p.delete('proforma')
p.delete('copy')
}),
{ scroll: false },
@@ -286,6 +292,14 @@ export default function InvoicesPage() {
}),
{ scroll: false },
)
const openNewProforma = () =>
router.push(
invoicesUrl((p) => {
p.set('new', '1')
p.set('proforma', '1')
}),
{ scroll: false },
)
const closeRotRutPayout = () =>
router.replace(invoicesUrl((p) => p.delete('rot-rut')), { scroll: false })
const openRotRutPayout = () =>
@@ -527,6 +541,15 @@ export default function InvoicesPage() {
disabledTitle: t('viewer_disabled_tooltip'),
onSelect: () => openNewQuote(),
},
{
key: 'proforma',
label: t('create_proforma'),
icon: FileClock,
description: t('create_proforma_desc'),
disabled: !canWrite,
disabledTitle: t('viewer_disabled_tooltip'),
onSelect: () => openNewProforma(),
},
{
key: 'aterkommande',
label: t('create_recurring'),
@@ -929,7 +952,7 @@ export default function InvoicesPage() {
open
copyFromId={copyFromId}
selfBilled={openSelfBilled}
documentType={openQuote ? 'quote' : undefined}
documentType={openQuote ? 'quote' : openProforma ? 'proforma' : undefined}
onOpenChange={(open) => {
if (!open) closeNewInvoice()
}}
+4 -2
View File
@@ -30,7 +30,7 @@ interface Props {
copyFromId?: string | null
/** Preselect the självfaktura tab (split-button entry on /invoices). */
selfBilled?: boolean
/** Preselect a document type, e.g. 'quote' ("Ny offert" split-button entry). */
/** Preselect a document type: 'quote' ("Ny offert") or 'proforma' ("Ny proformafaktura") split-button entries. */
documentType?: InvoiceDocumentType
}
@@ -146,7 +146,9 @@ export default function NewInvoiceDialog({
? t('title_copy')
: documentType === 'quote' && !selfBilled
? t('title_quote')
: t('title_invoice')}
: documentType === 'proforma' && !selfBilled
? t('title_proforma')
: t('title_invoice')}
</DialogTitle>
{copyFromId ? (
copyLoadFailed ? (
+2
View File
@@ -6465,6 +6465,8 @@
"create_self_desc": "Register a self-billing invoice from your customer",
"create_quote": "New quote",
"create_quote_desc": "Create a quote that can become an invoice",
"create_proforma": "New proforma invoice",
"create_proforma_desc": "Advance invoice that is not booked and can become a real invoice",
"bulkbar_selected": "{count, plural, one {invoice selected} other {invoices selected}}",
"bulk_select_all": "Select all ({count})",
"bulk_clear": "Clear",
+2
View File
@@ -6465,6 +6465,8 @@
"create_self_desc": "Registrera en självfaktura från din kund",
"create_quote": "Ny offert",
"create_quote_desc": "Skapa en offert som kan bli en faktura",
"create_proforma": "Ny proformafaktura",
"create_proforma_desc": "Förhandsfaktura som inte bokförs och kan bli en riktig faktura",
"bulkbar_selected": "{count, plural, one {faktura vald} other {fakturor valda}}",
"bulk_select_all": "Markera alla ({count})",
"bulk_clear": "Rensa",