fix(arsredovisning): make the aktiekapital note completable via compa… (#1118)
* fix(arsredovisning): make the aktiekapital note completable via company settings The annual report warned every AB that the aktiekapital note was missing and pointed at Installningar -> Foretag, but the referenced columns (aktiekapital, antal_aktier, kvotvarde) never existed and no settings UI was ever built, so the warning was a dead end and no AB could produce a complete note before Bolagsverket filing. - migration 20260723103000: company_settings.aktiekapital (numeric) and antal_aktier (integer) with positive CHECKs; kvotvarde is intentionally not stored since ABL 1 kap 6 defines it as aktiekapital / antal aktier - build-data.ts (K2 and K3 note paths): select only the two stored columns and derive kvotvarde with roundOre - UpdateSettingsSchema: aktiekapital (positive), antal_aktier (positive integer), both nullable to allow clearing - new ShareCapitalForm section on Installningar -> Foretag, rendered for aktiebolag only, with live derived kvotvarde display; wired through the existing CompanySettingsContent save path (empty string clears to null) - sv/en strings; settings route tests (round-trip, clear, 400 on invalid); builder tests for derived kvotvarde and the empty-settings warning Staging (metjnjrhvujscngnpzdv) already has the columns applied and the note verified end-to-end against a rehearsal company. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arsredovisning): address PR review findings on the share-capital note - enforce aktiekapital/antal_aktier as an all-or-nothing pair (DB CHECK, K2/K3 note guard now requires both, partial pair warns instead) - numeric(15,2) column, .int() Zod constraint, maxFractionDigits 0 render - guard numberOrNull against NaN; align kvotvarde preview with schema - strengthen clearing test, add fractional and partial-pair tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -310,3 +310,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-07-23] Validate preview-PDF payment settings before fetching customer data using the requested currency and document type: this preserves the same exemption semantics while minimizing personal-data processing for requests that cannot render.
|
||||
[2026-07-23] Retain an exact pending delivery snapshot when the provider succeeds but the terminal evidence RPC cannot be confirmed, and keep it outside the preparing-only reservation lock: inventing a sent state would be unsafe, while immutable payload, PDF, operator warnings, and later explicit resend availability preserve evidence and recovery.
|
||||
[2026-07-23] Keep the invoice-delivery DPIA as a documented screening rather than fabricating a full Article 35 assessment or DPO sign-off: the screened processing does not meet the high-risk threshold, and the implemented controls minimize routine access while preserving statutory evidence.
|
||||
[2026-07-23] Derive kvotvärde (aktiekapital / antal_aktier) in the annual-report note instead of adding the kvotvarde column the builder used to read: a stored third value could desync from the other two and file an internally inconsistent Bolagsverket note; ABL 1 kap 6 § makes it purely derived.
|
||||
|
||||
@@ -97,6 +97,54 @@ describe('PUT /api/settings', () => {
|
||||
expect(deadlineMocks.regenerate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('round-trips share capital fields and clears them with null', async () => {
|
||||
const updates = { aktiekapital: 25000, antal_aktier: 500 }
|
||||
enqueueMany([
|
||||
{ data: { entity_type: 'aktiebolag', onboarding_complete: true } },
|
||||
{ data: { id: 's1', ...updates } },
|
||||
{ data: null, count: 5 },
|
||||
])
|
||||
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: updates,
|
||||
}), { params: Promise.resolve({}) })
|
||||
const { status, body } = await parseJsonResponse<{ data: typeof updates }>(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body.data).toMatchObject(updates)
|
||||
|
||||
enqueueMany([
|
||||
{ data: { entity_type: 'aktiebolag', onboarding_complete: true } },
|
||||
{ data: { id: 's1', aktiekapital: null, antal_aktier: null } },
|
||||
{ data: null, count: 5 },
|
||||
])
|
||||
const clearResponse = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: { aktiekapital: null, antal_aktier: null },
|
||||
}), { params: Promise.resolve({}) })
|
||||
const cleared = await parseJsonResponse<{ data: Record<string, unknown> }>(clearResponse)
|
||||
expect(cleared.status).toBe(200)
|
||||
expect(cleared.body.data.aktiekapital).toBeNull()
|
||||
expect(cleared.body.data.antal_aktier).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects non-positive aktiekapital and fractional antal_aktier', async () => {
|
||||
for (const body of [
|
||||
{ aktiekapital: 0 },
|
||||
{ aktiekapital: -25000 },
|
||||
{ aktiekapital: 25000.5 },
|
||||
{ antal_aktier: 0 },
|
||||
{ antal_aktier: 500.5 },
|
||||
]) {
|
||||
const response = await PUT(createMockRequest('/api/settings', {
|
||||
method: 'PUT',
|
||||
body,
|
||||
}), { params: Promise.resolve({}) })
|
||||
expect((await parseJsonResponse(response)).status).toBe(400)
|
||||
}
|
||||
})
|
||||
|
||||
it('updates invoice email recipients and payment accounts', async () => {
|
||||
const updates = {
|
||||
invoice_email_cc_addresses: ['info@example.com', 'owner@example.com'],
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { roundOre } from '@/lib/money'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import type { CompanySettings } from '@/types'
|
||||
|
||||
interface ShareCapitalFormProps {
|
||||
settings: CompanySettings
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered share capital per Bolagsverket, feeding the statutory
|
||||
* aktiekapital note in the annual report. Kvotvärde (ABL 1 kap 6 §:
|
||||
* aktiekapital / antal aktier) is derived, never entered.
|
||||
*/
|
||||
export function ShareCapitalForm({ settings }: ShareCapitalFormProps) {
|
||||
const t = useTranslations('settings_company')
|
||||
const [aktiekapital, setAktiekapital] = useState(
|
||||
settings.aktiekapital != null ? String(settings.aktiekapital) : '',
|
||||
)
|
||||
const [antalAktier, setAntalAktier] = useState(
|
||||
settings.antal_aktier != null ? String(settings.antal_aktier) : '',
|
||||
)
|
||||
|
||||
const capital = Number(aktiekapital)
|
||||
const shares = Number(antalAktier)
|
||||
// Mirror UpdateSettingsSchema: whole-krona capital > 0, positive integer
|
||||
// share count. No preview for values the server would reject.
|
||||
const kvotvarde =
|
||||
Number.isSafeInteger(capital) && capital > 0 && Number.isSafeInteger(shares) && shares > 0
|
||||
? roundOre(capital / shares)
|
||||
: null
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{t('share_capital_heading')}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="aktiekapital">{t('aktiekapital_label')}</Label>
|
||||
<Input
|
||||
id="aktiekapital"
|
||||
name="aktiekapital"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min="1"
|
||||
step="1"
|
||||
value={aktiekapital}
|
||||
onChange={(e) => setAktiekapital(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('aktiekapital_help')}</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="antal_aktier">{t('antal_aktier_label')}</Label>
|
||||
<Input
|
||||
id="antal_aktier"
|
||||
name="antal_aktier"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min="1"
|
||||
step="1"
|
||||
value={antalAktier}
|
||||
onChange={(e) => setAntalAktier(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('antal_aktier_help')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{kvotvarde !== null && (
|
||||
<p className="text-xs text-muted-foreground tabular-nums">
|
||||
{t('kvotvarde_display', { value: formatCurrency(kvotvarde) })}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { LogoUpload } from '@/components/settings/LogoUpload'
|
||||
import { SettingsFormWrapper } from '@/components/settings/SettingsFormWrapper'
|
||||
import { SettingsLoadError } from '@/components/settings/SettingsLoadError'
|
||||
import { SettingsLoadingSkeleton } from '@/components/settings/SettingsLoadingSkeleton'
|
||||
import { ShareCapitalForm } from '@/components/settings/ShareCapitalForm'
|
||||
import { useSettings } from '@/components/settings/useSettings'
|
||||
import type { CompanySettings } from '@/types'
|
||||
|
||||
@@ -21,6 +22,14 @@ export function CompanySettingsContent() {
|
||||
if (!settings) return <SettingsLoadError onRetry={refetch} />
|
||||
|
||||
function handleSave(formData: FormData) {
|
||||
// Empty string clears the value (schema accepts null, not '').
|
||||
const numberOrNull = (name: string) => {
|
||||
const raw = String(formData.get(name) ?? '').trim()
|
||||
if (raw === '') return null
|
||||
const parsed = Number(raw)
|
||||
// NaN would serialize to null in JSON and silently clear the value.
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
const updates: Record<string, unknown> = {
|
||||
...(formData.has('company_name') && { company_name: formData.get('company_name') as string }),
|
||||
...(formData.has('org_number') && { org_number: formData.get('org_number') as string }),
|
||||
@@ -30,6 +39,8 @@ export function CompanySettingsContent() {
|
||||
phone: (formData.get('phone') as string) || '',
|
||||
email: (formData.get('email') as string) || '',
|
||||
website: (formData.get('website') as string) || '',
|
||||
...(formData.has('aktiekapital') && { aktiekapital: numberOrNull('aktiekapital') }),
|
||||
...(formData.has('antal_aktier') && { antal_aktier: numberOrNull('antal_aktier') }),
|
||||
}
|
||||
return {
|
||||
updates,
|
||||
@@ -48,6 +59,7 @@ export function CompanySettingsContent() {
|
||||
<div className="space-y-8">
|
||||
<SettingsFormWrapper onSave={handleSave} className="space-y-8">
|
||||
<CompanyInfoForm settings={settings} />
|
||||
{settings.entity_type === 'aktiebolag' && <ShareCapitalForm settings={settings} />}
|
||||
</SettingsFormWrapper>
|
||||
|
||||
<div className="border-t border-border pt-8">
|
||||
|
||||
@@ -1599,6 +1599,9 @@ export const UpdateSettingsSchema = z.object({
|
||||
tax_contact_email: z.string().email().nullable().optional().or(z.literal('')),
|
||||
fiscal_year_start_month: z.number().int().min(1).max(12).optional(),
|
||||
preliminary_tax_monthly: z.number().nullable().optional(),
|
||||
// Share capital per Bolagsverket (annual report aktiekapital note).
|
||||
aktiekapital: z.number().int('Aktiekapital anges i hela kronor').positive('Aktiekapital måste vara större än 0').nullable().optional(),
|
||||
antal_aktier: z.number().int('Antal aktier måste vara ett heltal').positive('Antal aktier måste vara större än 0').nullable().optional(),
|
||||
employer_registered: z.boolean().nullable().optional(),
|
||||
employer_seasonal: z.boolean().optional(),
|
||||
bank_name: z.string().max(100, 'Banknamn får vara max 100 tecken').nullable().optional(),
|
||||
|
||||
@@ -46,6 +46,7 @@ function makeSupabase(opts: {
|
||||
accountingFramework: 'k2' | 'k3'
|
||||
entityType?: string
|
||||
aktiekapital?: number | null
|
||||
antalAktier?: number | null
|
||||
agmDate?: string | null
|
||||
}): ChainableMock {
|
||||
const from = vi.fn((table: string) => {
|
||||
@@ -83,8 +84,12 @@ function makeSupabase(opts: {
|
||||
address: { city: 'Stockholm' },
|
||||
entity_type: opts.entityType ?? 'aktiebolag',
|
||||
aktiekapital: opts.aktiekapital ?? null,
|
||||
antal_aktier: opts.aktiekapital ? 500 : null,
|
||||
kvotvarde: opts.aktiekapital ? 100 : null,
|
||||
antal_aktier:
|
||||
opts.antalAktier !== undefined
|
||||
? opts.antalAktier
|
||||
: opts.aktiekapital
|
||||
? 500
|
||||
: null,
|
||||
},
|
||||
error: null,
|
||||
}),
|
||||
@@ -340,6 +345,37 @@ describe('buildArsredovisningData: K3', () => {
|
||||
).toBeDefined()
|
||||
})
|
||||
|
||||
it('derives kvotvärde in the aktiekapital note instead of reading a stored column', async () => {
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3', aktiekapital: 25_000 })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
const note = data.noter.find((n) => n.title === 'Aktiekapital')
|
||||
expect(note).toBeDefined()
|
||||
// 25 000 kr / 500 aktier per the settings mock (ABL 1 kap 6 §).
|
||||
expect(note!.body).toContain('Antal aktier: 500.')
|
||||
expect(note!.body).toContain('Kvotvärde per aktie: 50 kr.')
|
||||
})
|
||||
|
||||
it('warns instead of emitting an aktiekapital note when settings are empty', async () => {
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
expect(data.noter.find((n) => n.title === 'Aktiekapital')).toBeUndefined()
|
||||
expect(data.warnings.find((w) => w.startsWith('Aktiekapitalnoten saknas'))).toBeDefined()
|
||||
})
|
||||
|
||||
it('treats a partial share-capital pair as missing (warns, no note) for K3', async () => {
|
||||
const supabase = makeSupabase({
|
||||
accountingFramework: 'k3',
|
||||
aktiekapital: 25_000,
|
||||
antalAktier: null,
|
||||
})
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
expect(data.noter.find((n) => n.title === 'Aktiekapital')).toBeUndefined()
|
||||
expect(data.warnings.find((w) => w.startsWith('Aktiekapitalnoten saknas'))).toBeDefined()
|
||||
})
|
||||
|
||||
it('DROPS the old "K3 noter need manual augmentation" warning text', async () => {
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
@@ -421,6 +457,28 @@ describe('buildArsredovisningData: K2 byte-equivalence', () => {
|
||||
expect(principles!.body).toContain('BFNAR 2016:10')
|
||||
})
|
||||
|
||||
it('derives kvotvärde in the K2 aktiekapital note', async () => {
|
||||
const supabase = makeSupabase({ accountingFramework: 'k2', aktiekapital: 25_000 })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
const note = data.noter.find((n) => n.title === 'Aktiekapital')
|
||||
expect(note).toBeDefined()
|
||||
expect(note!.body).toContain('Antal aktier: 500.')
|
||||
expect(note!.body).toContain('Kvotvärde per aktie: 50 kr.')
|
||||
})
|
||||
|
||||
it('treats a partial share-capital pair as missing (warns, no note) for K2', async () => {
|
||||
const supabase = makeSupabase({
|
||||
accountingFramework: 'k2',
|
||||
aktiekapital: 25_000,
|
||||
antalAktier: null,
|
||||
})
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
expect(data.noter.find((n) => n.title === 'Aktiekapital')).toBeUndefined()
|
||||
expect(data.warnings.find((w) => w.startsWith('Aktiekapitalnoten saknas'))).toBeDefined()
|
||||
})
|
||||
|
||||
it('does NOT call generateKassaflodesanalys for K2', async () => {
|
||||
const supabase = makeSupabase({ accountingFramework: 'k2' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
|
||||
@@ -4,6 +4,7 @@ import { generateKassaflodesanalys } from '@/lib/reports/kassaflodesanalys'
|
||||
import { listAssets } from '@/lib/bokslut/assets/asset-service'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import { LATENT_TAX_DEFAULT_RATE } from '@/lib/bokslut/tax-provision/latent-tax-calculator'
|
||||
import { roundOre } from '@/lib/money'
|
||||
import {
|
||||
mapTrialBalancesToK2,
|
||||
type K2MappingResult,
|
||||
@@ -526,23 +527,27 @@ async function buildK2Noter(
|
||||
if (maybeAb) {
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('aktiekapital, antal_aktier, kvotvarde')
|
||||
.select('aktiekapital, antal_aktier')
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle()
|
||||
type AktiekapitalShape = { aktiekapital?: number | null; antal_aktier?: number | null; kvotvarde?: number | null }
|
||||
type AktiekapitalShape = { aktiekapital?: number | null; antal_aktier?: number | null }
|
||||
const ak = settings as AktiekapitalShape | null
|
||||
const aktiekapital = ak?.aktiekapital ?? null
|
||||
const antalAktier = ak?.antal_aktier ?? null
|
||||
const kvotvarde = ak?.kvotvarde ?? null
|
||||
if (aktiekapital || antalAktier) {
|
||||
const parts: string[] = []
|
||||
if (aktiekapital) parts.push(`Aktiekapital: ${aktiekapital.toLocaleString('sv-SE')} kr.`)
|
||||
if (antalAktier) parts.push(`Antal aktier: ${antalAktier.toLocaleString('sv-SE')}.`)
|
||||
if (kvotvarde) parts.push(`Kvotvärde per aktie: ${kvotvarde.toLocaleString('sv-SE')} kr.`)
|
||||
// Kvotvärde is defined (ABL 1 kap 6 §) as aktiekapital / antal aktier;
|
||||
// deriving it here keeps the filed note internally consistent. ÅRL
|
||||
// 5 kap 14 § requires BOTH the registered amount and the number of
|
||||
// shares, so a partial pair is treated as missing (warn, no note).
|
||||
if (aktiekapital && antalAktier) {
|
||||
const kvotvarde = roundOre(aktiekapital / antalAktier)
|
||||
notes.push({
|
||||
number: notes.length + 1,
|
||||
title: 'Aktiekapital',
|
||||
body: parts.join(' '),
|
||||
body: [
|
||||
`Aktiekapital: ${aktiekapital.toLocaleString('sv-SE', { maximumFractionDigits: 0 })} kr.`,
|
||||
`Antal aktier: ${antalAktier.toLocaleString('sv-SE')}.`,
|
||||
`Kvotvärde per aktie: ${kvotvarde.toLocaleString('sv-SE')} kr.`,
|
||||
].join(' '),
|
||||
})
|
||||
} else {
|
||||
// Don't write a "saknas: komplettera" placeholder into the PDF body: // that text would land in the Bolagsverket-filed document as a user-
|
||||
@@ -778,27 +783,30 @@ async function buildK3Noter(
|
||||
if (maybeAb) {
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('aktiekapital, antal_aktier, kvotvarde')
|
||||
.select('aktiekapital, antal_aktier')
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle()
|
||||
type AktiekapitalShape = {
|
||||
aktiekapital?: number | null
|
||||
antal_aktier?: number | null
|
||||
kvotvarde?: number | null
|
||||
}
|
||||
const ak = settings as AktiekapitalShape | null
|
||||
const aktiekapital = ak?.aktiekapital ?? null
|
||||
const antalAktier = ak?.antal_aktier ?? null
|
||||
const kvotvarde = ak?.kvotvarde ?? null
|
||||
if (aktiekapital || antalAktier) {
|
||||
const parts: string[] = []
|
||||
if (aktiekapital) parts.push(`Aktiekapital: ${aktiekapital.toLocaleString('sv-SE')} kr.`)
|
||||
if (antalAktier) parts.push(`Antal aktier: ${antalAktier.toLocaleString('sv-SE')}.`)
|
||||
if (kvotvarde) parts.push(`Kvotvärde per aktie: ${kvotvarde.toLocaleString('sv-SE')} kr.`)
|
||||
// Kvotvärde is defined (ABL 1 kap 6 §) as aktiekapital / antal aktier;
|
||||
// deriving it here keeps the filed note internally consistent. ÅRL
|
||||
// 5 kap 14 § requires BOTH the registered amount and the number of
|
||||
// shares, so a partial pair is treated as missing (warn, no note).
|
||||
if (aktiekapital && antalAktier) {
|
||||
const kvotvarde = roundOre(aktiekapital / antalAktier)
|
||||
notes.push({
|
||||
number: notes.length + 1,
|
||||
title: 'Aktiekapital',
|
||||
body: parts.join(' '),
|
||||
body: [
|
||||
`Aktiekapital: ${aktiekapital.toLocaleString('sv-SE', { maximumFractionDigits: 0 })} kr.`,
|
||||
`Antal aktier: ${antalAktier.toLocaleString('sv-SE')}.`,
|
||||
`Kvotvärde per aktie: ${kvotvarde.toLocaleString('sv-SE')} kr.`,
|
||||
].join(' '),
|
||||
})
|
||||
} else if (isAb) {
|
||||
warnings.push(
|
||||
|
||||
@@ -1322,6 +1322,12 @@
|
||||
},
|
||||
"settings_company": {
|
||||
"company_info_heading": "Company details",
|
||||
"share_capital_heading": "Share capital",
|
||||
"aktiekapital_label": "Share capital (SEK)",
|
||||
"aktiekapital_help": "Registered share capital per Bolagsverket. Used in the annual report note on share capital.",
|
||||
"antal_aktier_label": "Number of shares",
|
||||
"antal_aktier_help": "Total number of shares per Bolagsverket.",
|
||||
"kvotvarde_display": "Quota value per share: {value}",
|
||||
"company_name_label": "Company name",
|
||||
"company_name_help": "Shown on invoices, emails and declaration files. For a sole trader it is usually your own name (First Last).",
|
||||
"org_number_label": "Organisation number",
|
||||
|
||||
@@ -1322,6 +1322,12 @@
|
||||
},
|
||||
"settings_company": {
|
||||
"company_info_heading": "Företagsuppgifter",
|
||||
"share_capital_heading": "Aktiekapital",
|
||||
"aktiekapital_label": "Aktiekapital (kr)",
|
||||
"aktiekapital_help": "Registrerat aktiekapital enligt Bolagsverket. Används i årsredovisningens not om aktiekapital.",
|
||||
"antal_aktier_label": "Antal aktier",
|
||||
"antal_aktier_help": "Totalt antal aktier enligt Bolagsverket.",
|
||||
"kvotvarde_display": "Kvotvärde per aktie: {value}",
|
||||
"company_name_label": "Företagsnamn",
|
||||
"company_name_help": "Visas på fakturor, e-post och deklarationsfiler. För enskild firma är det vanligtvis ditt eget namn (Förnamn Efternamn).",
|
||||
"org_number_label": "Organisationsnummer",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Share-capital facts for the annual report aktiekapital note (K2/K3, ÅRL).
|
||||
-- The report builder has read these since the ÅR feature shipped, but the
|
||||
-- columns were never created, so the "fyll i under Inställningar → Företag"
|
||||
-- warning was a dead end for every AB. Kvotvärde is intentionally NOT stored:
|
||||
-- ABL 1 kap 6 § defines it as aktiekapital / antal aktier, so it is derived
|
||||
-- at read time to keep a Bolagsverket-filed note internally consistent.
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
ADD COLUMN IF NOT EXISTS aktiekapital numeric(15,2),
|
||||
ADD COLUMN IF NOT EXISTS antal_aktier integer;
|
||||
|
||||
COMMENT ON COLUMN public.company_settings.aktiekapital IS
|
||||
'Registered share capital in SEK per Bolagsverket. Used for the aktiekapital note in the annual report; may differ from the booked 2081 balance during a pending emission.';
|
||||
COMMENT ON COLUMN public.company_settings.antal_aktier IS
|
||||
'Total number of issued shares per Bolagsverket. Used for the aktiekapital note; kvotvärde is derived as aktiekapital / antal_aktier.';
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
DROP CONSTRAINT IF EXISTS company_settings_aktiekapital_positive;
|
||||
ALTER TABLE public.company_settings
|
||||
ADD CONSTRAINT company_settings_aktiekapital_positive
|
||||
CHECK (aktiekapital IS NULL OR aktiekapital > 0);
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
DROP CONSTRAINT IF EXISTS company_settings_antal_aktier_positive;
|
||||
ALTER TABLE public.company_settings
|
||||
ADD CONSTRAINT company_settings_antal_aktier_positive
|
||||
CHECK (antal_aktier IS NULL OR antal_aktier > 0);
|
||||
|
||||
-- The aktiekapital note (ÅRL 5 kap 14 §) needs both the registered amount
|
||||
-- and the number of shares; a lone value would produce an incomplete
|
||||
-- statutory note, so the pair is all-or-nothing.
|
||||
ALTER TABLE public.company_settings
|
||||
DROP CONSTRAINT IF EXISTS company_settings_share_capital_pair;
|
||||
ALTER TABLE public.company_settings
|
||||
ADD CONSTRAINT company_settings_share_capital_pair
|
||||
CHECK ((aktiekapital IS NULL) = (antal_aktier IS NULL));
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -284,6 +284,11 @@ export interface CompanySettings {
|
||||
// Preliminary tax
|
||||
preliminary_tax_monthly: number | null
|
||||
|
||||
// Share capital per Bolagsverket (aktiekapital note in the annual report).
|
||||
// Kvotvärde is derived as aktiekapital / antal_aktier, never stored.
|
||||
aktiekapital?: number | null
|
||||
antal_aktier?: number | null
|
||||
|
||||
// Bank details for invoices
|
||||
bank_name: string | null
|
||||
clearing_number: string | null
|
||||
|
||||
Reference in New Issue
Block a user