feat(invoices): allow BAS class 1-3 posting-account overrides and complete the aktiekapital note (#1121)
- invoice/article posting-account overrides accept active class 1-3 accounts; class 1-2 (balance-sheet) accounts are rejected on VAT-bearing lines so the ruta 05 tax base always books to a 3xxx account - shared posting-account regex across server schemas, pending-operation re-validation, and client forms - share-capital settings (aktiekapital/antal_aktier) feed the annual-report note; kvotvarde derived per ABL 1 kap 6 $; all-or-nothing pair constraint - signed per-rate VAT breakdown on credit-note PDFs; U+2212 to ASCII hyphen Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b0044bfe98
commit
43f7ccab9e
@@ -62,6 +62,7 @@ import LineDimensionFields from '@/components/dimensions/LineDimensionFields'
|
||||
import { DEFAULT_DEFERRED_REVENUE_ACCOUNT } from '@/lib/bookkeeping/accruals/account-suggestions'
|
||||
import { countCalendarMonths } from '@/lib/bookkeeping/accruals/compute'
|
||||
import type { InvoiceCopyInitial } from '@/lib/invoices/copy-invoice'
|
||||
import { INVOICE_POSTING_ACCOUNT_REGEX } from '@/lib/invoices/posting-account'
|
||||
import type { Customer, Currency, CreateInvoiceInput, CreateCustomerInput, InvoiceDocumentType, Article, Invoice, InvoiceItem, BASAccount } from '@/types'
|
||||
|
||||
const currencies: Currency[] = ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']
|
||||
@@ -166,7 +167,11 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
vat_rate: z.number().min(0).max(25),
|
||||
// Article linkage (artikelregister). Optional: free-text lines omit them.
|
||||
article_id: z.string().nullable().optional(),
|
||||
revenue_account: z.string().nullable().optional(),
|
||||
revenue_account: z
|
||||
.string()
|
||||
.regex(INVOICE_POSTING_ACCOUNT_REGEX, t('posting_account_invalid'))
|
||||
.nullable()
|
||||
.optional(),
|
||||
// ROT/RUT-avdrag per line. Optional: null means "no deduction".
|
||||
deduction_type: z.enum(['rot', 'rut']).nullable().optional(),
|
||||
labor_hours: z.number().nonnegative().nullable().optional(),
|
||||
@@ -294,9 +299,9 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
// Artikelregister: active articles for the line picker + which line is mid quick-create.
|
||||
const [articles, setArticles] = useState<ArticleOption[]>([])
|
||||
const [savingArticleIndex, setSavingArticleIndex] = useState<number | null>(null)
|
||||
// Class-3 (revenue) accounts for the optional per-line försäljningskonto
|
||||
// override, plus which rows currently show that picker.
|
||||
const [revenueAccounts, setRevenueAccounts] = useState<BASAccount[]>([])
|
||||
// Active balance-sheet and revenue accounts for the optional per-line
|
||||
// posting override, plus which rows currently show that picker.
|
||||
const [postingAccounts, setPostingAccounts] = useState<BASAccount[]>([])
|
||||
const [accountOverrideRows, setAccountOverrideRows] = useState<Set<number>>(new Set())
|
||||
// Dimension tagging (kostnadsställe/projekt, dimensions PR7). Affordances
|
||||
// render only when company_settings.dimensions_enabled: a UI-visibility
|
||||
@@ -497,9 +502,11 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
async function fetchRevenueAccounts() {
|
||||
if (!company?.id) return
|
||||
try {
|
||||
const res = await fetch('/api/bookkeeping/accounts?class=3')
|
||||
const res = await fetch('/api/bookkeeping/accounts')
|
||||
const body = await res.json()
|
||||
setRevenueAccounts((body?.data as BASAccount[]) || [])
|
||||
const accounts = ((body?.data as BASAccount[]) || [])
|
||||
.filter((account) => account.account_class >= 1 && account.account_class <= 3)
|
||||
setPostingAccounts(accounts)
|
||||
} catch {
|
||||
// Non-fatal: the override picker degrades to free 4-digit entry.
|
||||
}
|
||||
@@ -851,7 +858,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
}
|
||||
}
|
||||
|
||||
// Open/close the optional per-line försäljningskonto override. Closing clears
|
||||
// Open/close the optional per-line posting-account override. Closing clears
|
||||
// the value so the engine falls back to the VAT-rate-derived revenue account.
|
||||
function toggleAccountOverride(index: number) {
|
||||
const isOpen = accountOverrideRows.has(index) || !!watchItems[index]?.revenue_account
|
||||
@@ -1896,7 +1903,7 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Optional försäljningskonto override (engångsartikel). When
|
||||
{/* Optional posting-account override (engångsartikel). When
|
||||
unset the engine derives the revenue account from the VAT
|
||||
rate; reverse-charge/export lines ignore the override. */}
|
||||
{isInvoiceDoc && watchItems[index]?.line_type !== 'text' &&
|
||||
@@ -1913,11 +1920,16 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
|
||||
render={({ field }) => (
|
||||
<AccountCombobox
|
||||
value={field.value ?? ''}
|
||||
accounts={revenueAccounts}
|
||||
accounts={postingAccounts}
|
||||
onChange={(v) => field.onChange(v || null)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors.items?.[index]?.revenue_account && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.items[index].revenue_account?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('revenue_account_hint')}</p>
|
||||
|
||||
Reference in New Issue
Block a user