feat(deadlines): årsstämma replaces bokslut + moms_yearly auto-complete + EU-sales suggestion (#1059)

* feat(deadlines): gate F-skatt reminders on debited preliminary tax, add durable dismissal

The f_skatt deadline was gated on the F-skatt approval flag (DB default
true), giving nearly every company 12 monthly payment reminders for a tax
Skatteverket may not have debited at all (64% of all system deadline rows,
one lifetime completion). Approval carries no recurring obligation; the
monthly duty is payment of debiterad preliminarskatt and exists only while
the debited amount is > 0 (SFL 62 kap. 4-5 par., 55 kap. 2 par.).

- Gate the f_skatt deadline on preliminary_tax_monthly > 0 (field already
  collected at onboarding, previously unread) and retitle it as a payment.
- Storforetag keep the 12th in August (January-only 17th, 62 kap. 3 par.).
- Declare the prod-only preliminary_tax_monthly column in a migration so
  installs built purely from migrations stop failing tax-settings saves.
- Add deadlines.dismissed_at: DELETE on a system deadline now soft-dismisses
  it durably (hard deletes were resurrected by the nightly backfill within
  24h); generator, backfill, and every read surface respect it.
- Prune upcoming f_skatt rows for companies with no debited amount.

Closes part of #1028.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(deadlines): gate AGI on employer registration, stop completing AGI deadline at XML generation

The arbetsgivardeklaration deadline was gated on pays_salaries, which is
wrong in both directions: a registered employer must file AGI every month
including nil months (SFL 26 kap. 3 par.), and companies actively running
payroll with the flag off got no AGI reminders at all (each missed monthly
filing risks a forseningsavgift).

- New company_settings.employer_registered (nullable, no default) gates
  AGI and the storforetag skatteinbetalning row; pays_salaries remains a
  fallback for rows saved before the flag existed and keeps its UI meaning.
- Migration backfills employer_registered=true from pays_salaries=true and
  from actual payroll activity (salary_runs).
- New employer_seasonal flag: sasongsregistrerade file only for payment
  months plus a December nil declaration, so only the December-period row
  is generated.
- Settings UI: registration + seasonal checkboxes (sv/en strings).
- AGI XML generation no longer auto-completes the deadline as submitted:
  SFL 26 kap. deems the obligation satisfied only when the declaration has
  come in to Skatteverket. The Skatteverket extension's kvittens reconcile
  remains the confirming path; manual filers tick the deadline themselves.

Part of #1028.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(deadlines): statutory arsstamma replaces bokslut, moms_yearly auto-complete, EU-sales suggestion

- Replace the non-statutory 'bokslut' deadline (3 months after FY end, no
  legal basis, off-by-one month math for broken FYs) with the statutory
  arsstamma deadline: within 6 months of FY end per ABL 7 kap. 10 par.,
  the corporate act that gates the arsredovisning filing chain. Migration
  deletes pending bokslut rows; the backfill cron generates arsstamma rows.
- Complete moms_yearly on Skatteverket submission/kvittens: the yearly
  branch previously returned null with a stale comment claiming annual VAT
  has no deadline type, leaving yearly filers with an eternally open row.
  The fiscal-year tax_period label is derived from company settings.
- Add /api/settings/eu-trade-signal + a tax-settings callout: companies
  with booked EU sales (3108/3308/3107, last 15 months) but EU-trade/PS
  flags off are prompted to confirm the periodisk sammanstallning
  obligation (SFL 35 kap., 1 250 kr late fee per report). Suggestion only,
  never auto-enables.

Part of #1028.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deadlines): include dismissed_at in DeadlineForm payload

The Deadline type gained the required dismissed_at field; the form's
submit payload literal must carry it for the Omit<Deadline, ...> shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(deadlines): make system-deadline dismissal atomic

Constrain the dismiss update to source='system' and verify a row was
actually updated: a concurrent regeneration can delete the row between
lookup and update, and the route must not report a phantom success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-17 17:02:49 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent da4d5a39ae
commit 05b954ac1d
17 changed files with 297 additions and 45 deletions
+2
View File
@@ -213,3 +213,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-17] System-deadline delete = soft dismiss (dismissed_at) rather than a mute endpoint or hard delete: hard deletes were silently resurrected by the nightly backfill cron; dismissed rows satisfy the generator/backfill like completed rows.
[2026-07-17] AGI deadline gate = employer_registered (nullable, pays_salaries fallback) with migration backfill from salary_runs: a registered employer owes monthly AGI incl. nil months (SFL 26 kap. 3 §); companies running payroll in-app are treated as employers (SFL 7 kap. 1 § obliges registration), erring toward a dismissible reminder over a missed statutory filing. Seasonal employers get only the December-period row.
[2026-07-17] AGI XML generation no longer completes the arbetsgivardeklaration deadline: SFL 26 kap. deems the duty met only when the declaration reaches Skatteverket; kvittens reconcile remains the confirming path.
[2026-07-17] Removed 'bokslut' deadline type (replaced by statutory 'arsstamma', ABL 7:10, 6 months): the 3-month milestone had no legal basis and its broken-FY date math was off by one month (May-start FY got 31 Aug; Nov-start rolled "Feb 31" into March). Completed bokslut rows kept for history; type removed from the union like the earlier 'moms'/'inkomstdeklaration' retirements.
[2026-07-17] EU-trade/PS settings stay opt-in flags; a ledger-derived signal (postings on 3108/3308/3107, 15 months) only renders a suggestion callout in tax settings. Auto-flipping registration flags from ledger data would assert a Skatteverket registration we cannot know.
@@ -217,6 +217,25 @@ describe('VAT kvittenser cron', () => {
)
})
it('yearly picker params complete the moms_yearly deadline with the fiscal-year label', async () => {
mockCreateClient.mockReturnValueOnce(
stubHappyTables({ ...LOCKED_STATE, periodType: 'yearly', period: 12 }),
)
mockSkvRequest.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ kvittensnummer: 'KV-999' }),
} as any)
await GET(makeRequest())
// Calendar FY (company_settings unstubbed → default start month 1):
// the moms_yearly tax_period is the plain year label.
expect(mockCompleteTaxDeadline).toHaveBeenCalledWith(
expect.anything(), 'comp-1', ['moms_yearly'], '2026', 'confirmed',
)
})
it('legacy state without picker params still flips status but skips the deadline', async () => {
const { periodType: _pt, year: _y, period: _p, ...legacyState } = LOCKED_STATE
mockCreateClient.mockReturnValueOnce(stubHappyTables(legacyState))
@@ -192,20 +192,30 @@ export async function GET(request: Request) {
// carries the picker params (written by the one-click chain; states
// persisted by the older step-by-step routes lack them).
if (state.periodType && state.year && state.period) {
const taxPeriod =
state.periodType === 'monthly'
? `${state.year}-${String(state.period).padStart(2, '0')}`
: state.periodType === 'quarterly'
? `${state.year}-Q${state.period}`
: null
let taxPeriod: string | null = null
let deadlineTypes: ('moms_monthly' | 'moms_quarterly' | 'moms_yearly')[] = [
'moms_monthly',
'moms_quarterly',
]
if (state.periodType === 'monthly') {
taxPeriod = `${state.year}-${String(state.period).padStart(2, '0')}`
} else if (state.periodType === 'quarterly') {
taxPeriod = `${state.year}-Q${state.period}`
} else if (state.periodType === 'yearly') {
// moms_yearly rows carry the generator's fiscal-year label:
// `YYYY` for calendar FYs, `YYYY-1/YYYY` for broken ones.
const { data: fySettings } = await supabase
.from('company_settings')
.select('fiscal_year_start_month')
.eq('company_id', companyId)
.maybeSingle()
const startMonth = fySettings?.fiscal_year_start_month ?? 1
const yearNum = Number(state.year)
taxPeriod = startMonth === 1 ? `${yearNum}` : `${yearNum - 1}/${yearNum}`
deadlineTypes = ['moms_yearly']
}
if (taxPeriod) {
await completeTaxDeadline(
supabase,
companyId,
['moms_monthly', 'moms_quarterly'],
taxPeriod,
'confirmed'
)
await completeTaxDeadline(supabase, companyId, deadlineTypes, taxPeriod, 'confirmed')
}
}
@@ -0,0 +1,61 @@
/**
* Tests for /api/settings/eu-trade-signal — ledger-derived EU sales signal.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { NextResponse } from 'next/server'
import { createMockRequest, parseJsonResponse } from '@/tests/helpers'
const requireAuthMock = vi.fn()
vi.mock('@/lib/auth/require-auth', () => ({
requireAuth: (...args: unknown[]) => requireAuthMock(...args),
}))
vi.mock('@/lib/company/context', () => ({
getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
requireCompanyId: vi.fn().mockResolvedValue('company-1'),
}))
const fetchEntryLinesMock = vi.fn()
vi.mock('@/lib/bookkeeping/entry-lines', () => ({
fetchEntryLines: (...args: unknown[]) => fetchEntryLinesMock(...args),
}))
import { GET } from '../route'
beforeEach(() => {
vi.clearAllMocks()
})
describe('GET /api/settings/eu-trade-signal', () => {
it('returns 401 when not authenticated', async () => {
requireAuthMock.mockResolvedValue({
user: null,
supabase: {},
error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
})
const res = await GET(createMockRequest('/api/settings/eu-trade-signal'), {})
expect(res.status).toBe(401)
})
it('reports EU sales when the ledger has postings on the PS accounts', async () => {
requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase: {}, error: null })
fetchEntryLinesMock.mockResolvedValue([{ account_number: '3308' }])
const { status, body } = await parseJsonResponse<{ data: { has_eu_sales: boolean } }>(
await GET(createMockRequest('/api/settings/eu-trade-signal'), {})
)
expect(status).toBe(200)
expect(body.data.has_eu_sales).toBe(true)
})
it('reports no EU sales for an empty result', async () => {
requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase: {}, error: null })
fetchEntryLinesMock.mockResolvedValue([])
const { status, body } = await parseJsonResponse<{ data: { has_eu_sales: boolean } }>(
await GET(createMockRequest('/api/settings/eu-trade-signal'), {})
)
expect(status).toBe(200)
expect(body.data.has_eu_sales).toBe(false)
})
})
+43
View File
@@ -0,0 +1,43 @@
import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { fetchEntryLines, type EntryLinesQuery } from '@/lib/bookkeeping/entry-lines'
/**
* Accounts whose postings indicate EU B2B sales: 3108 goods, 3308 services,
* 3107 triangulation. Same set the periodisk sammanställning report keys on.
*/
const EU_SALES_ACCOUNTS = ['3108', '3308', '3107']
/** How far back to look for EU sales postings. */
const LOOKBACK_MONTHS = 15
/**
* GET /api/settings/eu-trade-signal
*
* Derived suggestion signal for the tax settings page: companies with EU
* sales in the ledger have a statutory periodisk sammanställning obligation
* (SFL 35 kap.), but the driving settings flags are opt-in and easily left
* off. The settings UI uses this to prompt the user to confirm EU trade and
* PS registration; it never flips the flags itself.
*/
export const GET = withRouteContext(
'settings.eu_trade_signal',
async (_request, { supabase, companyId }) => {
const since = new Date()
since.setMonth(since.getMonth() - LOOKBACK_MONTHS)
const sinceStr = since.toISOString().split('T')[0]
const lines = await fetchEntryLines<{ account_number: string }>({
supabase,
lineColumns: 'account_number',
filterEntries: (q: EntryLinesQuery) =>
q
.eq('company_id', companyId)
.in('status', ['posted', 'reversed'])
.gte('entry_date', sinceStr),
filterLines: (q: EntryLinesQuery) => q.in('account_number', EU_SALES_ACCOUNTS),
})
return NextResponse.json({ data: { has_eu_sales: lines.length > 0 } })
},
)
+12 -1
View File
@@ -10,9 +10,11 @@ import type { CompanySettings } from '@/types'
interface TaxSettingsFormProps {
settings: CompanySettings
/** Ledger-derived signal: EU sales postings exist (3108/3308/3107). */
euSalesDetected?: boolean
}
export function TaxSettingsForm({ settings }: TaxSettingsFormProps) {
export function TaxSettingsForm({ settings, euSalesDetected = false }: TaxSettingsFormProps) {
const t = useTranslations('settings_tax_form')
const [vatRegistered, setVatRegistered] = useState(settings.vat_registered ?? false)
const [fSkatt, setFSkatt] = useState(settings.f_skatt ?? true)
@@ -96,6 +98,15 @@ export function TaxSettingsForm({ settings }: TaxSettingsFormProps) {
</div>
</div>
{euSalesDetected && vatRegistered && (!hasEuTrade || !psEnabled) && (
<div className="rounded-lg border border-border p-4">
<p className="text-sm">{t('eu_trade_suggestion_title')}</p>
<p className="mt-1 text-xs text-muted-foreground">
{t('eu_trade_suggestion_help')}
</p>
</div>
)}
{vatRegistered && (
<div className="space-y-4 pl-7">
<div className="max-w-xs space-y-2">
@@ -1,6 +1,6 @@
'use client'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
import { useSearchParams, useRouter } from 'next/navigation'
import { TaxSettingsForm } from '@/components/settings/TaxSettingsForm'
@@ -22,6 +22,25 @@ export function TaxSettingsContent() {
const hasSkatteverketExtension = ENABLED_EXTENSION_IDS.has('skatteverket')
// Derived EU-sales signal: postings on 3108/3308/3107 imply a periodisk
// sammanställning obligation the opt-in flags may not reflect. Suggestion
// only; the user confirms via the ordinary checkboxes.
const [euSalesDetected, setEuSalesDetected] = useState(false)
useEffect(() => {
let cancelled = false
fetch('/api/settings/eu-trade-signal')
.then((res) => (res.ok ? res.json() : null))
.then((json) => {
if (!cancelled && json?.data?.has_eu_sales) setEuSalesDetected(true)
})
.catch(() => {
// Best-effort signal: a failed fetch just hides the suggestion.
})
return () => {
cancelled = true
}
}, [])
// Skatteverket OAuth callback: the connect flow returns to /settings/tax with
// a status query param (returnTo set in SkatteverketConnectPanel).
useEffect(() => {
@@ -108,7 +127,7 @@ export function TaxSettingsContent() {
{showSkatteverket && <SkatteverketConnectPanel />}
<SettingsFormWrapper onSave={handleSave} className="space-y-0">
<TaxSettingsForm settings={settings} />
<TaxSettingsForm settings={settings} euSalesDetected={euSalesDetected} />
</SettingsFormWrapper>
</div>
)
+27 -7
View File
@@ -2278,8 +2278,8 @@ function parseQueryParams(
/**
* Build the deadline generator's tax_period string (`YYYY-MM` monthly,
* `YYYY-QN` quarterly) from the picker params. Annual VAT has no system
* deadline type, so yearly returns null and the deadline hook is a no-op.
* `YYYY-QN` quarterly) from the picker params. Yearly periods use the
* fiscal-year label and need company settings; see yearlyVatTaxPeriod.
*/
function vatTaxPeriod(periodType: VatPeriodType, year: number, period: number): string | null {
if (periodType === 'monthly') return `${year}-${String(period).padStart(2, '0')}`
@@ -2287,11 +2287,28 @@ function vatTaxPeriod(periodType: VatPeriodType, year: number, period: number):
return null
}
/**
* The moms_yearly row's tax_period is the generator's fiscal-year label:
* `YYYY` for calendar fiscal years and `YYYY-1/YYYY` for broken ones (year
* = the FY-end year). Derived from company settings because the picker only
* carries the year.
*/
async function yearlyVatTaxPeriod(ctx: ExtensionContext, year: number): Promise<string> {
const { data } = await ctx.supabase
.from('company_settings')
.select('fiscal_year_start_month')
.eq('company_id', ctx.companyId)
.maybeSingle()
const startMonth = data?.fiscal_year_start_month ?? 1
return startMonth === 1 ? `${year}` : `${year - 1}/${year}`
}
/**
* Complete the moms deadline for the period identified by the request's
* optional periodType/year/period query params. Both moms deadline types are
* passed: company settings decide which one exists, the other is a no-op.
* Best-effort by design (completeTaxDeadline never throws).
* optional periodType/year/period query params. Both monthly and quarterly
* types are passed for sub-annual periods: company settings decide which one
* exists, the other is a no-op. Best-effort by design (completeTaxDeadline
* never throws).
*/
async function completeVatDeadlineFromRequest(
request: Request,
@@ -2305,12 +2322,15 @@ async function completeVatDeadlineFromRequest(
if (!periodType || !Number.isFinite(year) || !Number.isFinite(period) || !year || !period) {
return
}
const taxPeriod = vatTaxPeriod(periodType, year, period)
const taxPeriod =
periodType === 'yearly'
? await yearlyVatTaxPeriod(ctx, year)
: vatTaxPeriod(periodType, year, period)
if (!taxPeriod) return
await completeTaxDeadline(
ctx.supabase,
ctx.companyId,
['moms_monthly', 'moms_quarterly'],
periodType === 'yearly' ? ['moms_yearly'] : ['moms_monthly', 'moms_quarterly'],
taxPeriod,
newStatus
)
+3 -1
View File
@@ -248,11 +248,13 @@ describe('Enum schemas', () => {
const types = [
'moms_monthly', 'moms_quarterly', 'moms_yearly', 'f_skatt',
'arbetsgivardeklaration', 'inkomstdeklaration_ef', 'inkomstdeklaration_ab',
'arsredovisning', 'periodisk_sammanstallning', 'bokslut',
'arsredovisning', 'arsstamma', 'periodisk_sammanstallning',
]
for (const t of types) {
expect(TaxDeadlineTypeSchema.safeParse(t).success).toBe(true)
}
// Retired: replaced by the statutory arsstamma deadline (ABL 7:10).
expect(TaxDeadlineTypeSchema.safeParse('bokslut').success).toBe(false)
})
it('NormalBalanceSchema and MappingRuleTypeSchema', () => {
+1 -1
View File
@@ -260,8 +260,8 @@ export const TaxDeadlineTypeSchema = z.enum([
'inkomstdeklaration_ef',
'inkomstdeklaration_ab',
'arsredovisning',
'arsstamma',
'periodisk_sammanstallning',
'bokslut',
])
export const DeadlineSourceSchema = z.enum(['system', 'user'])
+1 -1
View File
@@ -182,8 +182,8 @@ function getSwedishTaxTypeLabel(type: string): string {
inkomstdeklaration_ef: 'Inkomstdeklaration EF',
inkomstdeklaration_ab: 'Inkomstdeklaration AB',
arsredovisning: 'Årsredovisning',
arsstamma: 'Årsstämma',
periodisk_sammanstallning: 'Periodisk sammanställning',
bokslut: 'Bokslut',
}
return labels[type] || type
}
+32
View File
@@ -296,6 +296,38 @@ describe('inkomstdeklaration_ab: digital filing deadlines', () => {
})
})
describe('arsstamma: 6 months after FY end (ABL 7:10)', () => {
const config = getConfig('arsstamma')
it('applies only to aktiebolag', () => {
expect(config.condition(makeSettings())).toBe(true)
expect(config.condition(makeSettings({ entity_type: 'enskild_firma' }))).toBe(false)
})
it('FY end Dec (calendar year) → Jun 30 next year', () => {
const dates = config.generateDates(2026, makeSettings({ fiscal_year_start_month: 1 }))
expect(dates.length).toBe(1)
expect(dates[0]).toMatchObject({ day: 30, month: 5, year: 2026, period: '2025' })
})
it('FY end Apr → Oct 31 same year, broken-FY period label', () => {
const dates = config.generateDates(2026, makeSettings({ fiscal_year_start_month: 5 }))
expect(dates.length).toBe(1)
expect(dates[0]).toMatchObject({ day: 31, month: 9, year: 2026, period: '2025/2026' })
})
it('uses the last day of the deadline month (handles Feb)', () => {
// FY end Aug 2026 → +6 months = Feb 2027
const dates = config.generateDates(2027, makeSettings({ fiscal_year_start_month: 9 }))
expect(dates.length).toBe(1)
expect(dates[0]).toMatchObject({ day: 28, month: 1, year: 2027 })
})
it('no bokslut deadline type exists anymore', () => {
expect(TAX_DEADLINE_CONFIGS.find((c) => (c.type as string) === 'bokslut')).toBeUndefined()
})
})
describe('arsredovisning: 7 months after FY end (ÅRL 8:3)', () => {
const config = getConfig('arsredovisning')
+30 -16
View File
@@ -441,28 +441,42 @@ export const TAX_DEADLINE_CONFIGS: TaxDeadlineConfig[] = [
},
},
// Bokslut (AB) - 31 mars for calendar year
// Årsstämma (AB): within 6 months of FY end per ABL 7 kap. 10 §. Replaces
// the former non-statutory 'bokslut' milestone (3 months had no legal
// basis). The stämma gates the årsredovisning chain: the AR is presented
// and adopted there, and the Bolagsverket filing (arsredovisning row,
// 7 months) requires the adopted AR.
{
type: 'bokslut',
titleTemplate: 'Bokslut räkenskapsår {periodLabel}',
description: 'Bokslut för aktiebolag',
type: 'arsstamma',
titleTemplate: 'Årsstämma räkenskapsår {periodLabel}',
description: 'Årsstämma för aktiebolag (senast sex månader efter räkenskapsårets utgång)',
condition: (s) => s.entity_type === 'aktiebolag',
priority: 'important',
linkedReportType: null,
generateDates: (year, settings) => {
// For calendar year, December 31 is fiscal year end, deadline March 31
if (settings.fiscal_year_start_month === 1) {
return [
{ day: 31, month: 2, year, period: `${year - 1}`, periodLabel: `${year - 1}` }, // March
]
// FY end month (1-indexed)
const fyEndMonth = settings.fiscal_year_start_month === 1 ? 12 : settings.fiscal_year_start_month - 1
// Last day of (FY end month + 6). Swedish fiscal years always end on
// the last day of a calendar month (BFL 3 kap.), so this equals the
// statutory six-month limit.
const results: DeadlineInstance[] = []
for (const endYr of [year - 1, year]) {
const dlMonth0 = ((fyEndMonth - 1) + 6) % 12
const dlYear = (fyEndMonth - 1) + 6 >= 12 ? endYr + 1 : endYr
if (dlYear === year) {
const lastDay = new Date(dlYear, dlMonth0 + 1, 0).getDate()
const periodLabel = fyEndMonth === 12 ? `${endYr}` : `${endYr - 1}/${endYr}`
results.push({
day: lastDay,
month: dlMonth0,
year: dlYear,
period: periodLabel,
periodLabel,
})
}
}
// For non-calendar fiscal years, 3 months after year end
const fiscalYearEnd = settings.fiscal_year_start_month - 1
const deadlineMonth = (fiscalYearEnd + 3) % 12
const deadlineYear = deadlineMonth < fiscalYearEnd ? year + 1 : year
return [
{ day: 31, month: deadlineMonth, year: deadlineYear, period: `${year - 1}/${year}`, periodLabel: `${year - 1}/${year}` },
]
return results
},
},
]
+2
View File
@@ -1780,6 +1780,8 @@
"pays_salaries_help": "Controls the salary module in the menu. Paying out salary requires employer registration with Skatteverket.",
"employer_registered_label": "Registered as employer",
"employer_registered_help": "Registered employers must file an employer declaration (AGI) every month, including months without salary payments (nil declaration). Controls AGI deadlines.",
"eu_trade_suggestion_title": "Your bookkeeping contains EU sales",
"eu_trade_suggestion_help": "There are booked EU sales (accounts 3108, 3308 or 3107) in the last 15 months, but EU trade or the EU sales list is not enabled below. The EU sales list (periodisk sammanställning) must be filed monthly or quarterly when selling to businesses in other EU countries; the late fee is 1,250 kr per report.",
"employer_seasonal_label": "Seasonal employer (säsongsregistrerad)",
"employer_seasonal_help": "Files the employer declaration only for months with salary payments, plus December if no salary was paid during the year.",
"preliminary_tax_heading": "Preliminary tax",
+2
View File
@@ -1780,6 +1780,8 @@
"pays_salaries_help": "Styr lönemodulen i menyn. Att betala ut lön kräver arbetsgivarregistrering hos Skatteverket.",
"employer_registered_label": "Registrerad som arbetsgivare",
"employer_registered_help": "Registrerade arbetsgivare ska lämna arbetsgivardeklaration varje månad, även månader utan löneutbetalning (nolldeklaration). Styr AGI-deadlines.",
"eu_trade_suggestion_title": "Bokföringen innehåller EU-försäljning",
"eu_trade_suggestion_help": "Det finns bokförda EU-försäljningar (konto 3108, 3308 eller 3107) de senaste 15 månaderna, men EU-handel eller periodisk sammanställning är inte aktiverad nedan. Periodisk sammanställning ska lämnas varje månad eller kvartal vid försäljning till företag i andra EU-länder; förseningsavgiften är 1 250 kr per rapport.",
"employer_seasonal_label": "Säsongsregistrerad arbetsgivare",
"employer_seasonal_help": "Lämnar arbetsgivardeklaration bara för månader med löneutbetalning, samt för december om ingen lön betalats under året.",
"preliminary_tax_heading": "Preliminärskatt",
@@ -0,0 +1,15 @@
-- Remove the non-statutory 'bokslut' deadline (issue #1028).
--
-- The "3 months after FY end" bokslut deadline had no legal basis (the
-- statutory anchors for AB are the arsstamma within 6 months, ABL 7 kap.
-- 10 §, and the Bolagsverket filing within 7 months, ARL 8 kap.), and its
-- non-calendar-FY date math was off by one month (a May-start FY produced
-- 31 Aug instead of 31 Jul; a Nov-start FY produced "Feb 31" which rolled
-- into March). The generator now produces an 'arsstamma' deadline instead;
-- the daily backfill cron creates those rows on its next run.
--
-- Completed rows are kept for history; pending rows are template noise.
DELETE FROM public.deadlines
WHERE source = 'system'
AND tax_deadline_type = 'bokslut'
AND is_completed = false;
+3 -3
View File
@@ -2174,8 +2174,8 @@ export type TaxDeadlineType =
| 'inkomstdeklaration_ef'
| 'inkomstdeklaration_ab'
| 'arsredovisning'
| 'arsstamma'
| 'periodisk_sammanstallning'
| 'bokslut'
// Deadline status workflow
export type DeadlineStatus =
@@ -2352,8 +2352,8 @@ export const TAX_DEADLINE_TYPE_LABELS: Record<TaxDeadlineType, string> = {
inkomstdeklaration_ef: 'Inkomstdeklaration EF',
inkomstdeklaration_ab: 'Inkomstdeklaration AB',
arsredovisning: 'Årsredovisning',
periodisk_sammanstallning: 'Periodisk sammanställning',
bokslut: 'Bokslut'
arsstamma: 'Årsstämma',
periodisk_sammanstallning: 'Periodisk sammanställning'
}
// ============================================================