diff --git a/DECISIONS.md b/DECISIONS.md index 7ff04449..6f20ca15 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -365,3 +365,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-24] Onboarding journey migration COMPLETE with PR #1150: wizard deleted, /companies/new on journey mode='add', BankID picker = searchable list, flag conditional removed (env var cleaned from Vercel post-merge). Bot-review triage: compliance findings on getUser()/redirect()/ensure_user_team skipped as App Router misreadings or pre-existing patterns; fixed the real ones (stale select_company keys, unused hasExistingCompanies plumbing). [2026-07-24] Invite recovery on onboarding surfaces = cookie retry + hint, NOT accept-by-email: a BankID signup's email is confirmed via a client-delivered magiclink (no mailbox proof), so auto-joining on email match would let anyone who registers the invitee's address claim the membership. The cookie/token path keeps mailbox possession required; cookie-less invitees get pointed back to the mailed link, with no company name leaked. [2026-07-24] Share-capital pair rule validated in the PUT route, not UpdateSettingsSchema: the all-or-nothing check needs the stored row (a partial update may send only one key), which Zod cannot see; the route already owns the other cross-field effective-value checks. +[2026-07-24] VAT RC checks proportional (0.5% + 1 kr tolerance) + latched stepper landing: the binary present/absent RC_BASIS_MISSING check cleared after one korrigering and hid a 38-voucher worklist behind "klart"; tolerance absorbs per-voucher basis rounding (moms/sats vs invoiced amount) without hiding a missing voucher; landing step latches once per period so a mid-work refetch cannot navigate the user off Kontrollera. diff --git a/app/api/reports/vat-declaration/rc-basis-gaps/__tests__/route.test.ts b/app/api/reports/vat-declaration/rc-basis-gaps/__tests__/route.test.ts new file mode 100644 index 00000000..095ed43b --- /dev/null +++ b/app/api/reports/vat-declaration/rc-basis-gaps/__tests__/route.test.ts @@ -0,0 +1,122 @@ +/** + * Tests for GET /api/reports/vat-declaration/rc-basis-gaps. + * + * Exercises the route through the real withRouteContext wrapper, mocking its + * auth/company dependencies and findRcBasisGaps(). Covers: 401, missing and + * invalid params, the happy path, and that fiscal_period_id is forwarded so + * yearly (helårsmoms) worklists cover the räkenskapsår instead of the + * calendar year. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { NextResponse } from 'next/server' +import { createQueuedMockSupabase, createMockRequest, parseJsonResponse } from '@/tests/helpers' + +const { supabase, reset } = createQueuedMockSupabase() + +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 findRcBasisGapsMock = vi.fn() +vi.mock('@/lib/reports/rc-basis-gaps', () => ({ + findRcBasisGaps: (...args: unknown[]) => findRcBasisGapsMock(...args), +})) + +import { GET } from '../route' + +function gapsRequest(searchParams: Record) { + return createMockRequest('/api/reports/vat-declaration/rc-basis-gaps', { searchParams }) +} + +const validParams = { periodType: 'quarterly', year: '2026', period: '2' } + +describe('GET /api/reports/vat-declaration/rc-basis-gaps', () => { + beforeEach(() => { + vi.clearAllMocks() + reset() + requireAuthMock.mockResolvedValue({ user: { id: 'user-1' }, supabase }) + findRcBasisGapsMock.mockResolvedValue([]) + }) + + it('returns 401 when unauthenticated', async () => { + requireAuthMock.mockResolvedValue({ + user: null, + supabase, + error: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }), + }) + + const response = await GET(gapsRequest(validParams), { params: Promise.resolve({}) }) + expect(response.status).toBe(401) + expect(findRcBasisGapsMock).not.toHaveBeenCalled() + }) + + it('returns 400 when period params are missing', async () => { + const response = await GET( + gapsRequest({ periodType: 'quarterly' }), + { params: Promise.resolve({}) }, + ) + const { status } = await parseJsonResponse(response) + expect(status).toBe(400) + }) + + it('returns 400 for an invalid periodType', async () => { + const response = await GET( + gapsRequest({ ...validParams, periodType: 'weekly' }), + { params: Promise.resolve({}) }, + ) + const { status, body } = await parseJsonResponse<{ error: { code: string } }>(response) + expect(status).toBe(400) + expect(body.error.code).toBe('VAT_REPORT_INVALID_PERIOD_TYPE') + }) + + it('returns the detected gaps (happy path)', async () => { + const gap = { + entryId: 'entry-1', + voucherNumber: 8, + voucherSeries: 'A', + entryDate: '2026-05-20', + description: 'Greptile Apr-Maj', + rcOutputAccount: '2614', + rcOutputAmount: 527.29, + expectedBasisAmount: 2109.16, + suggestedBasisAccount: '4535', + rate: 0.25, + } + findRcBasisGapsMock.mockResolvedValue([gap]) + + const response = await GET(gapsRequest(validParams), { params: Promise.resolve({}) }) + const { status, body } = await parseJsonResponse<{ data: { gaps: unknown[] } }>(response) + + expect(status).toBe(200) + expect(body.data.gaps).toEqual([gap]) + expect(findRcBasisGapsMock).toHaveBeenCalledWith(supabase, 'company-1', 'quarterly', 2026, 2, { + fiscalPeriodId: undefined, + }) + }) + + it('forwards fiscal_period_id for yearly declarations', async () => { + const response = await GET( + gapsRequest({ periodType: 'yearly', year: '2026', period: '1', fiscal_period_id: 'fp-1' }), + { params: Promise.resolve({}) }, + ) + expect(response.status).toBe(200) + expect(findRcBasisGapsMock).toHaveBeenCalledWith(supabase, 'company-1', 'yearly', 2026, 1, { + fiscalPeriodId: 'fp-1', + }) + }) + + it('maps a lib failure to VAT_REPORT_GENERATION_FAILED', async () => { + findRcBasisGapsMock.mockRejectedValue(new Error('boom')) + + const response = await GET(gapsRequest(validParams), { params: Promise.resolve({}) }) + const { status, body } = await parseJsonResponse<{ error: { code: string } }>(response) + expect(status).toBeGreaterThanOrEqual(500) + expect(body.error.code).toBe('VAT_REPORT_GENERATION_FAILED') + }) +}) diff --git a/app/api/reports/vat-declaration/rc-basis-gaps/route.ts b/app/api/reports/vat-declaration/rc-basis-gaps/route.ts index 9fe33213..2efcc497 100644 --- a/app/api/reports/vat-declaration/rc-basis-gaps/route.ts +++ b/app/api/reports/vat-declaration/rc-basis-gaps/route.ts @@ -14,6 +14,9 @@ export const GET = withRouteContext( const periodType = searchParams.get('periodType') as VatPeriodType | null const yearStr = searchParams.get('year') const periodStr = searchParams.get('period') + // Yearly (helårsmoms) resolves against the räkenskapsår, mirroring the + // declaration route; monthly/quarterly ignore it. + const fiscalPeriodId = searchParams.get('fiscal_period_id') ?? undefined if (!periodType || !yearStr || !periodStr) { return errorResponseFromCode('VAT_REPORT_MISSING_PARAMS', log, { requestId }) @@ -35,7 +38,9 @@ export const GET = withRouteContext( } try { - const gaps = await findRcBasisGaps(supabase, companyId, periodType, year, period) + const gaps = await findRcBasisGaps(supabase, companyId, periodType, year, period, { + fiscalPeriodId, + }) return NextResponse.json({ data: { gaps } }) } catch (err) { log.error('rc-basis-gaps detection failed', err as Error, { periodType, year, period }) diff --git a/components/reports/VatChecksCard.tsx b/components/reports/VatChecksCard.tsx index 4a75021d..9fba9167 100644 --- a/components/reports/VatChecksCard.tsx +++ b/components/reports/VatChecksCard.tsx @@ -107,10 +107,9 @@ export function VatChecksCard({ // Gap fetch tagged with the key it was requested under: loading is derived // by comparing tags, so the effect never sets state synchronously. Fixed // rows are removed via removedIds (the fetch itself is not re-run after a - // korrigering: the period key is unchanged). The key is period-only, NOT - // gated on RC_BASIS_MISSING: the aggregate check clears as soon as ONE - // voucher is corrected, and the remaining unfixed rows must survive the - // declaration refetch that follows each korrigering. + // korrigering: the period key is unchanged). The key is period-only so the + // list survives the declaration refetch that follows each korrigering, and + // survives a remount even when the aggregate check no longer fires. const gapsKey = `${periodType}:${year}:${period}:${fiscalPeriodId ?? ''}` const [gapsResult, setGapsResult] = useState<{ key: string @@ -145,25 +144,26 @@ export function VatChecksCard({ const gapsFetched = gapsResult !== null && gapsResult.key === gapsKey useEffect(() => { - // Fetch once per period, and only while the aggregate check flags a gap: - // the fetched list then outlives the check, which clears after the first - // correction even though other vouchers may remain broken. - if (!hasRcBasisGaps || gapsFetched) return + // Fetch once per period, NOT gated on the aggregate check: the check + // compares period totals and can clear (or never fire) while individual + // vouchers still miss their basis pair. Gating the fetch on it meant a + // remount after the first korrigering showed "inga fel" with the + // remaining broken vouchers silently hidden. + if (gapsFetched) return let cancelled = false const params = new URLSearchParams({ periodType, year: String(year), period: String(period), }) - // Forward-compatible: the route reads calendar params today, but yearly - // (helårsmoms) declarations resolve against the räkenskapsår. + // Yearly (helårsmoms) resolves against the räkenskapsår server-side. if (fiscalPeriodId) params.set('fiscal_period_id', fiscalPeriodId) fetch(`/api/reports/vat-declaration/rc-basis-gaps?${params.toString()}`) .then(async (r) => { const j = await r.json().catch(() => null) if (cancelled) return - // A failed fetch must not masquerade as "no gaps found": the check - // above says there ARE gaps, so an empty list here would mislead. + // A failed fetch must not masquerade as "no gaps found": an empty + // list would mislead whenever gaps actually exist. if (!r.ok || j?.error) setGapsResult({ key: gapsKey, gaps: [], failed: true }) else setGapsResult({ key: gapsKey, gaps: j?.data?.gaps || [] }) }) @@ -173,7 +173,7 @@ export function VatChecksCard({ return () => { cancelled = true } - }, [hasRcBasisGaps, gapsFetched, gapsKey, periodType, year, period, fiscalPeriodId]) + }, [gapsFetched, gapsKey, periodType, year, period, fiscalPeriodId]) const gaps = gapsFetched ? gapsResult.gaps.filter((g) => !removedIds.has(g.entryId)) diff --git a/components/reports/views/index.tsx b/components/reports/views/index.tsx index dac83815..801605e7 100644 --- a/components/reports/views/index.tsx +++ b/components/reports/views/index.tsx @@ -1627,6 +1627,16 @@ export function VatDeclarationView() { const checksBlocked = checks.some((c) => c.status === 'ERROR') const errorCount = checks.filter((c) => c.status === 'ERROR').length const warningCount = checks.filter((c) => c.status === 'WARNING').length + // Latch the automatic landing step once per period, as a render-phase + // adjustment when the period's declaration first settles. Deriving it live + // from checksBlocked navigated the user away mid-work: the refetch after a + // korrigering can clear the aggregate error while the Kontrollera worklist + // still holds broken vouchers, and the view would jump to Granska under + // their feet. The period-change effect below resets chosenStep to null, + // which re-arms this latch for the next period. + if (chosenStep === null && upToDate && data && !error) { + setChosenStep(checksBlocked ? 1 : 2) + } const activeStep = chosenStep ?? (checksBlocked ? 1 : 2) // Settings not settled yet — the picker defaults and the gate both depend diff --git a/lib/reports/__tests__/rc-basis-gaps.test.ts b/lib/reports/__tests__/rc-basis-gaps.test.ts new file mode 100644 index 00000000..1f60c910 --- /dev/null +++ b/lib/reports/__tests__/rc-basis-gaps.test.ts @@ -0,0 +1,127 @@ +/** + * Tests for findRcBasisGaps: per-voucher FK004 detection. + * + * Mocks the entry-lines fetch layer and resolvePeriodDates. The period must + * resolve through resolvePeriodDates (not the calendar arithmetic) so yearly + * (helårsmoms) worklists cover extended/broken räkenskapsår: a calendar span + * hid gap vouchers that the declaration totals still included. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { SupabaseClient } from '@supabase/supabase-js' + +const resolvePeriodDatesMock = vi.fn() +vi.mock('../vat-declaration', () => ({ + resolvePeriodDates: (...args: unknown[]) => resolvePeriodDatesMock(...args), +})) + +const fetchEntryLinesMock = vi.fn() +const fetchLinesByEntryIdsMock = vi.fn() +vi.mock('@/lib/bookkeeping/entry-lines', () => ({ + fetchEntryLines: (...args: unknown[]) => fetchEntryLinesMock(...args), + fetchLinesByEntryIds: (...args: unknown[]) => fetchLinesByEntryIdsMock(...args), +})) + +import { findRcBasisGaps } from '../rc-basis-gaps' + +const supabase = {} as SupabaseClient + +function rcLine(entryId: string, voucherNumber: number, credit: number) { + return { + journal_entry_id: entryId, + account_number: '2614', + debit_amount: 0, + credit_amount: credit, + journal_entries: { + id: entryId, + voucher_number: voucherNumber, + voucher_series: 'A', + entry_date: '2026-05-20', + description: `Voucher ${voucherNumber}`, + }, + } +} + +describe('findRcBasisGaps', () => { + beforeEach(() => { + vi.clearAllMocks() + resolvePeriodDatesMock.mockResolvedValue({ start: '2025-07-17', end: '2026-12-31' }) + fetchEntryLinesMock.mockResolvedValue([]) + fetchLinesByEntryIdsMock.mockResolvedValue([]) + }) + + it('resolves the period via resolvePeriodDates with the fiscal period id', async () => { + await findRcBasisGaps(supabase, 'company-1', 'yearly', 2026, 1, { fiscalPeriodId: 'fp-1' }) + + expect(resolvePeriodDatesMock).toHaveBeenCalledWith( + supabase, 'company-1', 'yearly', 2026, 1, 'fp-1', + ) + }) + + it('filters entries on the resolved bounds, not the calendar year', async () => { + await findRcBasisGaps(supabase, 'company-1', 'yearly', 2026, 1, { fiscalPeriodId: 'fp-1' }) + + const { filterEntries } = fetchEntryLinesMock.mock.calls[0][0] + const calls: Array<[string, ...unknown[]]> = [] + const q = new Proxy( + {}, + { + get: + (_t, method: string) => + (...args: unknown[]) => { + calls.push([method, ...args]) + return q + }, + }, + ) + filterEntries(q) + + expect(calls).toContainEqual(['gte', 'entry_date', '2025-07-17']) + expect(calls).toContainEqual(['lte', 'entry_date', '2026-12-31']) + }) + + it('flags vouchers whose RC output VAT lacks a matching basis pair', async () => { + fetchEntryLinesMock.mockResolvedValue([ + rcLine('entry-1', 8, 527.29), // no basis lines at all + rcLine('entry-2', 9, 250), // fully booked basis + ]) + fetchLinesByEntryIdsMock.mockResolvedValue([ + { + id: 'l-1', + journal_entry_id: 'entry-2', + account_number: '4535', + debit_amount: 1000, + credit_amount: 0, + }, + ]) + + const gaps = await findRcBasisGaps(supabase, 'company-1', 'monthly', 2026, 5) + + expect(gaps).toHaveLength(1) + expect(gaps[0]).toMatchObject({ + entryId: 'entry-1', + voucherNumber: 8, + rcOutputAccount: '2614', + rcOutputAmount: 527.29, + expectedBasisAmount: 2109.16, + suggestedBasisAccount: '4535', + rate: 0.25, + }) + }) + + it('flags a voucher whose basis is materially short of the expected amount', async () => { + fetchEntryLinesMock.mockResolvedValue([rcLine('entry-1', 8, 2500)]) + fetchLinesByEntryIdsMock.mockResolvedValue([ + { + id: 'l-1', + journal_entry_id: 'entry-1', + account_number: '4535', + debit_amount: 4000, // expected 10000 + credit_amount: 0, + }, + ]) + + const gaps = await findRcBasisGaps(supabase, 'company-1', 'monthly', 2026, 5) + expect(gaps).toHaveLength(1) + expect(gaps[0].expectedBasisAmount).toBe(10000) + }) +}) diff --git a/lib/reports/__tests__/vat-declaration-checks.test.ts b/lib/reports/__tests__/vat-declaration-checks.test.ts index e898c0dd..0f1b3582 100644 --- a/lib/reports/__tests__/vat-declaration-checks.test.ts +++ b/lib/reports/__tests__/vat-declaration-checks.test.ts @@ -53,6 +53,61 @@ describe('runVatDeclarationChecks', () => { expect(fk004?.message).toMatch(/ruta 20-24/) }) + // Regression (2026-07-24): the check compared presence, not proportion, so + // correcting ONE voucher out of ~39 cleared the error while ~51 tkr of + // basis was still missing and the declaration claimed to be ready. + it('flags ERROR when RC basis is only partially reported', () => { + const rutor: VatDeclarationRutor = { + ...emptyRutor, + ruta21: 2109.16, // one corrected voucher + ruta30: 13446.18, // fiktiv moms for ~39 vouchers → expects ~53 785 kr basis + ruta48: 13446.18, + ruta49: 0, + } + const finding = runVatDeclarationChecks(rutor).find((f) => f.code === 'RC_BASIS_MISSING') + expect(finding?.status).toBe('ERROR') + expect(finding?.message).toMatch(/saknas/) + }) + + it('does not flag RC basis inside the per-voucher rounding tolerance', () => { + const rutor: VatDeclarationRutor = { + ...emptyRutor, + ruta21: 9990, // expected 10000, tolerance max(1, 0.5%) = 50 + ruta30: 2500, + ruta48: 2500, + ruta49: 0, + } + const findings = runVatDeclarationChecks(rutor) + expect(findings.find((f) => f.code === 'RC_BASIS_MISSING')).toBeUndefined() + expect(findings.find((f) => f.code === 'RC_OUTPUT_MISSING')).toBeUndefined() + }) + + it('flags ERROR when a whole voucher of basis is missing beyond the tolerance', () => { + const rutor: VatDeclarationRutor = { + ...emptyRutor, + ruta21: 9000, // expected 10000: one ~1000 kr voucher missing + ruta30: 2500, + ruta48: 2500, + ruta49: 0, + } + expect( + runVatDeclarationChecks(rutor).find((f) => f.code === 'RC_BASIS_MISSING')?.status, + ).toBe('ERROR') + }) + + it('flags ERROR when basis exceeds what the output VAT accounts for', () => { + const rutor: VatDeclarationRutor = { + ...emptyRutor, + ruta21: 60000, // expected only 10000 from ruta30: fiktiv moms missing + ruta30: 2500, + ruta48: 2500, + ruta49: 0, + } + expect( + runVatDeclarationChecks(rutor).find((f) => f.code === 'RC_OUTPUT_MISSING')?.status, + ).toBe('ERROR') + }) + it('flags ERROR when basis is present but no output RC VAT', () => { const rutor: VatDeclarationRutor = { ...emptyRutor, diff --git a/lib/reports/rc-basis-gaps.ts b/lib/reports/rc-basis-gaps.ts index 6b099126..0340502a 100644 --- a/lib/reports/rc-basis-gaps.ts +++ b/lib/reports/rc-basis-gaps.ts @@ -4,7 +4,7 @@ import { fetchLinesByEntryIds, type EntryLinesQuery, } from '@/lib/bookkeeping/entry-lines' -import { calculatePeriodDates } from './vat-declaration' +import { resolvePeriodDates } from './vat-declaration' import type { VatPeriodType } from '@/types' /** @@ -107,8 +107,15 @@ export async function findRcBasisGaps( periodType: VatPeriodType, year: number, period: number, + options: { fiscalPeriodId?: string } = {}, ): Promise { - const { start, end } = calculatePeriodDates(periodType, year, period) + // Same period resolution as the declaration itself: helårsmoms covers the + // räkenskapsår, not the calendar year, so a calendar span would hide gap + // vouchers from the tail of an extended/broken fiscal year while the + // declaration totals (and the aggregate check) still include them. + const { start, end } = await resolvePeriodDates( + supabase, companyId, periodType, year, period, options.fiscalPeriodId + ) // Two-step entry-lines fetch (see lib/bookkeeping/entry-lines.ts). const rcLines = (await fetchEntryLines({ diff --git a/lib/reports/vat-declaration-checks.ts b/lib/reports/vat-declaration-checks.ts index 295e4c45..e81a6a99 100644 --- a/lib/reports/vat-declaration-checks.ts +++ b/lib/reports/vat-declaration-checks.ts @@ -72,32 +72,50 @@ export function runVatDeclarationChecks(rutor: VatDeclarationRutor): VatDeclarat // and we don't want a 0.01 rounding scrap to trip a sanity check. const eps = 0.5 - // FK004 mirror: output RC VAT exists, basis missing. - if (rcOutput > eps && rcBasis <= eps) { + // The basis the per-rate output boxes imply (basbelopp = fiktiv moms / sats). + // The RC checks compare reported basis against this instead of testing mere + // presence: a binary present/absent test clears as soon as ONE voucher in + // the period carries a basis pair, silently passing a declaration where the + // remaining vouchers still under-report rutor 20-24 (FK004). + const expectedRcBasis = + rutor.ruta30 / 0.25 + rutor.ruta31 / 0.12 + rutor.ruta32 / 0.06 + // Per-voucher öre rounding (basis derived as moms/sats vs the invoiced + // amount) accumulates with voucher count; 0.5% with a 1 kr floor absorbs + // that without hiding a genuinely missing voucher. + const rcTolerance = Math.max(1, expectedRcBasis * 0.005) + + // FK004 mirror: output RC VAT exists, basis missing or too low. + if (rcOutput > eps && rcBasis + rcTolerance < expectedRcBasis) { + const shortfall = Math.round(expectedRcBasis - rcBasis) findings.push({ code: 'RC_BASIS_MISSING', status: 'ERROR', message: - 'Du har redovisat utgående moms på inköp (ruta 30-32) men inget ' + - 'basbelopp för omvänd skattskyldighet (ruta 20-24). Skatteverket ' + - 'kräver att båda sidor finns med (ML 13 kap; SKV felkod FK004). ' + - 'Kontrollera att leverantörsfakturor med omvänd skattskyldighet ' + - 'är bokförda med basbelopp på 44xx/45xx-konton.', + 'Den utgående momsen på inköp (ruta 30-32) motsvarar ett basbelopp ' + + `på cirka ${Math.round(expectedRcBasis).toLocaleString('sv-SE')} kr, ` + + 'men ruta 20-24 innehåller bara ' + + `${Math.round(rcBasis).toLocaleString('sv-SE')} kr: cirka ` + + `${shortfall.toLocaleString('sv-SE')} kr saknas. Skatteverket kräver ` + + 'att båda sidor finns med (ML 13 kap; SKV felkod FK004). Kontrollera ' + + 'att leverantörsfakturor med omvänd skattskyldighet är bokförda med ' + + 'basbelopp på 44xx/45xx-konton.', rutor: ['ruta20', 'ruta21', 'ruta22', 'ruta23', 'ruta24', 'ruta30', 'ruta31', 'ruta32'], }) } - // Mirror: basis present but no output VAT, equally broken, often a - // half-finished manual posting. - if (rcBasis > eps && rcOutput <= eps) { + // Mirror: more basis than the output VAT accounts for, fiktiv moms missing + // for some vouchers. Covers both the all-output-missing case and a partial + // one; often a half-finished manual posting. + if (rcBasis > eps && rcBasis > expectedRcBasis + rcTolerance) { findings.push({ code: 'RC_OUTPUT_MISSING', status: 'ERROR', message: - 'Du har redovisat basbelopp för omvänd skattskyldighet (ruta 20-24) ' + - 'men ingen utgående moms (ruta 30-32). Vid omvänd skattskyldighet ' + - 'måste köparen redovisa både underlag och fiktiv moms (ML 13 kap). ' + - 'Kontrollera att fiktiv moms är bokförd på 2614/2624/2634.', + 'Basbeloppet för omvänd skattskyldighet (ruta 20-24) är större än ' + + 'vad den utgående momsen (ruta 30-32) motsvarar. Vid omvänd ' + + 'skattskyldighet måste köparen redovisa både underlag och fiktiv ' + + 'moms (ML 13 kap). Kontrollera att fiktiv moms är bokförd på ' + + '2614/2624/2634 för varje inköp.', rutor: ['ruta20', 'ruta21', 'ruta22', 'ruta23', 'ruta24', 'ruta30', 'ruta31', 'ruta32'], }) }