diff --git a/DECISIONS.md b/DECISIONS.md index fc233875..ca36a241 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1047,3 +1047,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-17] 77xx nedskrivningar split per official BAS kopplingstabell in BOTH k2-mapper and ink2-engine (fältkod 7515: 7700-7739, 7750-7789, 7800-7899; 7516: 774x, 779x): agent feedback 2026-07-07 reported the K2 side; the INK2R side and the swedish-sru-filing reference table had the same whole-77xx-to-7516 error, verified against bas.se INK2_P1_intervall-240118.pdf before overriding the skill reference. NE-bilaga mappings deliberately untouched (NE has no separate omsättningstillgångar line). [2026-08-17] MCP feedback loop = local /loop-feedback-triage appending dev_docs/mcp_feedback_digest.md + small PRs, NOT a GitHub-issue digest or Resend email: closes the loops.md backlog item blocked since 07-09 on a "channel decision". Issues stay founder-authorised; the digest is the read surface. gnubok_feedback reply copy no longer promises weekly aggregation (it was never true); tool advertised in server instructions + agent briefing (feedback_channel), where it was previously discoverable only by scanning tools/list. [2026-08-17] Non-IBAN foreign payment accounts (USD/GBP): added generic bank_code + foreign_account_number to InvoicePaymentAccount (JSONB, no migration) instead of per-country fields (routing_number, sort_code, bsb); rule = IBAN OR (bank_code + foreign_account_number + BIC), only for NON_IBAN_CURRENCIES, label per currency. Chosen over a field per country: the Currency union only carries USD/GBP among non-IBAN systems, and one generic pair keeps the PDF/settings/schema surface small; extend NON_IBAN_CURRENCIES + bankCodeLabelKey when AUD/CAD land. Agent feedback 2026-08-03. +[2026-08-18] Skattekontoutdrag sum mismatch (opening + events != closing) demoted from a hard 400 to a preview confirm gate showing ingående/händelser/utgående/differens, mirroring the orgnr-mismatch gate: Sebastian's real export was refused on it (2026-08-18) with no way forward and no figures to diagnose; nothing is booked at import and dedup makes a later complete re-import safe, so refusing the file only blocked the rows that WERE readable. Parser also takes the earliest opening / latest closing across several marker pairs, reads a marker saldo from a trailing running-saldo column, and accepts U+2212 / plus-sign amounts; the route logs the figures (amounts and counts, never row text) so the next report is diagnosable from Vercel logs. Kept the hard reject only for zero readable rows. diff --git a/app/api/import/skattekonto-file/parse/__tests__/route.test.ts b/app/api/import/skattekonto-file/parse/__tests__/route.test.ts index 5a796cd8..f8c46788 100644 --- a/app/api/import/skattekonto-file/parse/__tests__/route.test.ts +++ b/app/api/import/skattekonto-file/parse/__tests__/route.test.ts @@ -92,14 +92,35 @@ describe('POST /api/import/skattekonto-file/parse', () => { expect(body.error.code).toBe('SKATTEKONTO_FILE_NOT_RECOGNIZED') }) - it('rejects a statement that does not sum', async () => { + it('returns a statement that does not sum with the gap for the preview gate', async () => { enqueue({ data: null }) // no prior import + enqueue({ data: { org_number: '556677-8899' } }) // company_settings + enqueue({ data: [] }) // existing rows page const broken = MODERN_CSV.replace('"23 490"', '"99 999"') const { status, body } = await jsonOf( await POST(makeFileRequest(broken, 'Kontoutdrag 556677-8899 2026-05-03--2026-08-01.csv'), emptyParams), ) + expect(status).toBe(200) + expect(body.data.parse_result.sum_valid).toBe(false) + expect(body.data.parse_result.opening_saldo).toBe(-500) + expect(body.data.parse_result.events_sum).toBe(23490) + expect(body.data.parse_result.closing_saldo).toBe(99999) + expect(body.data.parse_result.sum_difference).toBe(76509) + // The rows are still returned: nothing is booked at import, so the user + // can import the events that ARE in the file and confirm the gap. + expect(body.data.parse_result.rows).toHaveLength(2) + }) + + it('still refuses a statement with no readable events', async () => { + enqueue({ data: null }) // no prior import + const empty = [ + '"Testbolaget AB";"556677-8899";""', + '"";"Ingående saldo 2026-05-03";"-500"', + '"";"Utgående saldo 2026-08-01";"-500"', + ].join('\r\n') + const { status, body } = await jsonOf(await POST(makeFileRequest(empty), emptyParams)) expect(status).toBe(400) - expect(body.error.code).toBe('SKATTEKONTO_FILE_SUM_MISMATCH') + expect(body.error.code).toBe('SKATTEKONTO_FILE_NO_ROWS') }) it('parses a valid statement and partitions against existing rows', async () => { diff --git a/app/api/import/skattekonto-file/parse/route.ts b/app/api/import/skattekonto-file/parse/route.ts index 8d8b3fac..32f360de 100644 --- a/app/api/import/skattekonto-file/parse/route.ts +++ b/app/api/import/skattekonto-file/parse/route.ts @@ -75,19 +75,29 @@ export const POST = withRouteContext( const parseResult = parseSkattekontoFile(content, file.name) - if (parseResult.sum_valid === false) { - return errorResponseFromCode('SKATTEKONTO_FILE_SUM_MISMATCH', opLog, { - requestId, - details: { - openingSaldo: parseResult.opening_saldo, - closingSaldo: parseResult.closing_saldo, - }, - }) - } if (parseResult.rows.length === 0) { return errorResponseFromCode('SKATTEKONTO_FILE_NO_ROWS', opLog, { requestId }) } + // A statement that does not sum is no longer refused: the preview shows + // the gap and asks for confirmation (see parser.ts). Log the figures so + // a report of "utdraget summerar inte" can be diagnosed without the + // file: amounts and counts only, never row text. + if (parseResult.sum_valid === false) { + opLog.warn('skattekonto file does not sum', { + openingSaldo: parseResult.opening_saldo, + closingSaldo: parseResult.closing_saldo, + eventsSum: parseResult.events_sum, + sumDifference: parseResult.sum_difference, + parsedRows: parseResult.stats.parsed_rows, + skippedRows: parseResult.stats.skipped_rows, + unreadableAmountRows: parseResult.stats.unreadable_amount_rows, + dateFrom: parseResult.date_from, + dateTo: parseResult.date_to, + variant: parseResult.variant, + }) + } + // Wrong-company guard: the modern export names its orgnr in the header // row. A mismatch is surfaced for the preview step to confirm, not a // hard block (legacy files have no header at all). diff --git a/components/import/SkattekontoFilePreviewStep.tsx b/components/import/SkattekontoFilePreviewStep.tsx index ad014af4..5868939c 100644 --- a/components/import/SkattekontoFilePreviewStep.tsx +++ b/components/import/SkattekontoFilePreviewStep.tsx @@ -15,6 +15,7 @@ import { } from '@/components/ui/table' import { ArrowLeft, ArrowRight, AlertTriangle, Calendar, FileText, Scale } from 'lucide-react' import { formatCurrency, cn } from '@/lib/utils' +import { roundOre } from '@/lib/money' import type { SkattekontoFileParseResult } from '@/lib/import/skattekonto-file/types' interface SkattekontoFilePreviewStepProps { @@ -40,13 +41,33 @@ export default function SkattekontoFilePreviewStep({ }: SkattekontoFilePreviewStepProps) { const t = useTranslations('import') const [mismatchConfirmed, setMismatchConfirmed] = useState(false) - const { rows, stats, issues, date_from, date_to, closing_saldo } = parseResult + const [sumGapConfirmed, setSumGapConfirmed] = useState(false) + const { + rows, + stats, + issues, + date_from, + date_to, + opening_saldo, + closing_saldo, + events_sum, + sum_difference, + sum_valid, + } = parseResult const duplicateSet = new Set(duplicateIndexes) const promotionSet = new Set(promotionIndexes) const newCount = rows.length - duplicateIndexes.length const warnings = issues.filter((i) => i.severity !== 'error') - const importBlocked = orgNumberMismatch && !mismatchConfirmed + // A statement that does not sum (truncated, filtered, unreadable rows) is + // a confirm gate, not a block: nothing is booked at import and every event + // is reviewed on the skattekonto page, so importing what IS in the file is + // safe. The gate exists so the user knows the picture is incomplete. + const sumGap = sum_valid === false + const sumGapHasFigures = + opening_saldo !== null && closing_saldo !== null && events_sum !== null && sum_difference !== null + const importBlocked = + (orgNumberMismatch && !mismatchConfirmed) || (sumGap && !sumGapConfirmed) return (
@@ -119,12 +140,61 @@ export default function SkattekontoFilePreviewStep({ )} + {sumGap && ( + + + + + {t('skattekonto_sum_gap_title')} + + + + {sumGapHasFigures ? ( +
+
{t('skattekonto_sum_gap_opening')}
+
{formatCurrency(opening_saldo)}
+
{t('skattekonto_sum_gap_events')}
+
{formatCurrency(roundOre(events_sum - opening_saldo))}
+
{t('skattekonto_sum_gap_expected')}
+
{formatCurrency(events_sum)}
+
{t('skattekonto_sum_gap_closing')}
+
{formatCurrency(closing_saldo)}
+
{t('skattekonto_sum_gap_difference')}
+
{formatCurrency(sum_difference)}
+
+ ) : ( +

{t('skattekonto_sum_gap_no_saldo')}

+ )} +

+ {stats.unreadable_amount_rows > 0 + ? t('skattekonto_sum_gap_body_unreadable', { count: stats.unreadable_amount_rows }) + : t('skattekonto_sum_gap_body')} +

+ +
+
+ )} + {duplicateIndexes.length > 0 && (

{t('skattekonto_duplicates_note', { count: duplicateIndexes.length })}

)} + {/* What happens after import: nothing is booked automatically, and an + event that already has a 1630 verifikat (a deposit booked from the + bank side) is offered as a link, not a second booking. Answers the + "some of these are already booked" question before the click. */} +

{t('skattekonto_after_import_note')}

+ {warnings.length > 0 && ( diff --git a/lib/errors/structured-errors.ts b/lib/errors/structured-errors.ts index adf82156..11b0b6ee 100644 --- a/lib/errors/structured-errors.ts +++ b/lib/errors/structured-errors.ts @@ -1723,13 +1723,6 @@ const SKATTEKONTO_FILE: Record = { message_en: 'The file was not recognized as a tax account statement. Download the account events from Skatteverket and try again.', }, - SKATTEKONTO_FILE_SUM_MISMATCH: { - httpStatus: 400, - message_sv: - 'Utdraget summerar inte: ingående saldo plus händelser stämmer inte med utgående saldo. Filen kan vara ofullständig.', - message_en: - 'The statement does not sum: opening balance plus events does not equal the closing balance. The file may be incomplete.', - }, SKATTEKONTO_FILE_NO_ROWS: { httpStatus: 400, message_sv: 'Kontoutdraget innehåller inga händelser att importera.', diff --git a/lib/import/skattekonto-file/__tests__/parser.test.ts b/lib/import/skattekonto-file/__tests__/parser.test.ts index 271c20c3..c1567104 100644 --- a/lib/import/skattekonto-file/__tests__/parser.test.ts +++ b/lib/import/skattekonto-file/__tests__/parser.test.ts @@ -98,7 +98,12 @@ describe('parseSkattekontoFile: modern export', () => { it('parses all transaction rows', () => { expect(result.rows).toHaveLength(9) - expect(result.stats).toEqual({ total_rows: 9, parsed_rows: 9, skipped_rows: 0 }) + expect(result.stats).toEqual({ + total_rows: 9, + parsed_rows: 9, + skipped_rows: 0, + unreadable_amount_rows: 0, + }) expect(result.variant).toBe('csv') }) @@ -126,6 +131,8 @@ describe('parseSkattekontoFile: modern export', () => { it('validates the sum invariant', () => { expect(result.sum_valid).toBe(true) + expect(result.events_sum).toBe(35087) + expect(result.sum_difference).toBe(0) expect(result.issues.filter((i) => i.severity === 'error')).toHaveLength(0) }) @@ -170,16 +177,94 @@ describe('parseSkattekontoFile: robustness', () => { expect(result.issues.some((i) => i.severity === 'error')).toBe(true) }) - it('flags a sum mismatch as an error', () => { + it('flags a sum mismatch as an error and reports the gap', () => { const truncated = MODERN_CSV.replace( '"2026-07-28";"Inbetalning bokförd 260727";"35 000"\r\n', '', ) const result = parseSkattekontoFile(truncated, MODERN_FILENAME) expect(result.sum_valid).toBe(false) + expect(result.events_sum).toBe(87) + expect(result.sum_difference).toBe(35000) + expect(result.rows).toHaveLength(8) expect(result.issues.some((i) => i.severity === 'error')).toBe(true) }) + it('counts dated rows with unreadable amounts as missing from the sum', () => { + const garbledRow = MODERN_CSV.replace( + '"2026-07-28";"Inbetalning bokförd 260727";"35 000"', + '"2026-07-28";"Inbetalning bokförd 260727";"trasigt"', + ) + const result = parseSkattekontoFile(garbledRow, MODERN_FILENAME) + expect(result.sum_valid).toBe(false) + expect(result.stats.unreadable_amount_rows).toBe(1) + expect(result.stats.skipped_rows).toBe(1) + const error = result.issues.find((i) => i.severity === 'error') + expect(error?.message).toContain('oläsbart belopp') + }) + + it('reads a typographic minus and an explicit plus sign', () => { + const typographic = [ + '"2026-06-06";"Kostnadsränta";"−10"', + '"2026-06-07";"Kostnadsränta";"–10"', + '"2026-07-11";"Inbetalning bokförd 260710";"+24 000"', + ].join('\n') + const result = parseSkattekontoFile(typographic, 'export.csv') + expect(result.rows.map((r) => r.belopp)).toEqual([-10, -10, 24000]) + expect(result.stats.skipped_rows).toBe(0) + }) + + it('reads marker saldo from a trailing running-saldo column', () => { + const withSaldoColumn = [ + '"Testbolaget AB";"556677-8899";"";""', + '"";"Ingående saldo 2026-05-03";"";"-500"', + '"2026-06-06";"Kostnadsränta";"-10";"-510"', + '"2026-07-11";"Inbetalning bokförd 260710";"24 000";"23 490"', + '"";"Utgående saldo 2026-08-01";"";"23 490"', + ].join('\r\n') + const result = parseSkattekontoFile(withSaldoColumn, 'export.csv') + expect(result.opening_saldo).toBe(-500) + expect(result.closing_saldo).toBe(23490) + expect(result.rows.map((r) => r.belopp)).toEqual([-10, 24000]) + expect(result.sum_valid).toBe(true) + }) + + it('checks a multi-section statement from the earliest opening to the latest closing', () => { + const multiYear = [ + '"Testbolaget AB";"556677-8899";""', + '"";"Ingående saldo 2025-01-01";"100"', + '"2025-03-12";"Debiterad preliminärskatt";"-8 000"', + '"2025-03-14";"Inbetalning bokförd 250313";"8 000"', + '"";"Utgående saldo 2025-12-31";"100"', + '"";"Ingående saldo 2026-01-01";"100"', + '"2026-02-12";"Debiterad preliminärskatt";"-9 000"', + '"2026-02-13";"Inbetalning bokförd 260212";"9 500"', + '"";"Utgående saldo 2026-08-01";"600"', + ].join('\r\n') + const result = parseSkattekontoFile(multiYear, 'export.csv') + expect(result.opening_saldo).toBe(100) + expect(result.closing_saldo).toBe(600) + expect(result.rows).toHaveLength(4) + expect(result.sum_valid).toBe(true) + }) + + it('orders markers by their own date when the file lists newest first', () => { + const newestFirst = [ + '"";"Utgående saldo 2026-08-01";"600"', + '"2026-02-13";"Inbetalning bokförd 260212";"9 500"', + '"2026-02-12";"Debiterad preliminärskatt";"-9 000"', + '"";"Ingående saldo 2026-01-01";"100"', + '"";"Utgående saldo 2025-12-31";"100"', + '"2025-03-14";"Inbetalning bokförd 250313";"8 000"', + '"2025-03-12";"Debiterad preliminärskatt";"-8 000"', + '"";"Ingående saldo 2025-01-01";"100"', + ].join('\r\n') + const result = parseSkattekontoFile(newestFirst, 'export.csv') + expect(result.opening_saldo).toBe(100) + expect(result.closing_saldo).toBe(600) + expect(result.sum_valid).toBe(true) + }) + it('skips malformed rows with warnings', () => { const withBad = [ '"2026-06-06";"Kostnadsränta";"-10"', diff --git a/lib/import/skattekonto-file/parser.ts b/lib/import/skattekonto-file/parser.ts index 6d68885e..a38dbfcc 100644 --- a/lib/import/skattekonto-file/parser.ts +++ b/lib/import/skattekonto-file/parser.ts @@ -20,7 +20,9 @@ * Secondary tolerance: legacy `.skv` text exports from the retired * e-service. Same date;text;amount row shape but possibly unquoted, without * the name/orgnr header, and sometimes with a trailing running-saldo column, - * which is ignored. + * which is ignored for event rows (a marker row whose belopp cell is empty + * takes its saldo from that column instead). Several marker pairs (one per + * year or page) are reduced to the earliest opening and latest closing. */ import { roundOre } from '@/lib/money' @@ -57,19 +59,44 @@ const SKV_VOCABULARY = [ /** * Parse a skattekonto amount: whole kronor or comma decimals, space/nbsp - * thousands separators, optional trailing "kr". Returns null on non-amounts. + * thousands separators, optional trailing "kr", optional explicit "+". + * Typographic minus variants (U+2212 MINUS SIGN, the CLDR sv-SE default, + * plus hyphen/dash lookalikes) count as a minus. Returns null on non-amounts. */ function parseAmount(value: string): number | null { const cleaned = value // \s covers regular space, nbsp (U+00A0) and narrow nbsp (U+202F). .replace(/\s/g, '') .replace(/kr$/i, '') + // U+2212 minus sign, U+2010..U+2013 hyphen/dash lookalikes. + .replace(/^[\u2212\u2010-\u2013]/, '-') + .replace(/^\+/, '') .replace(',', '.') if (cleaned === '' || cleaned === '-') return null if (!/^-?\d+(\.\d+)?$/.test(cleaned)) return null return roundOre(parseFloat(cleaned)) } +/** + * Amount of a saldo marker row. The Kontoutdrag export puts it in the + * belopp column; a layout with a trailing running-saldo column leaves belopp + * empty and carries the saldo in the last column. Take the last readable + * amount at or after the belopp column. + */ +function parseMarkerAmount(cells: string[]): number | null { + for (let i = cells.length - 1; i >= 2; i--) { + const amount = parseAmount(cells[i]) + if (amount !== null) return amount + } + return null +} + +/** Date written into a marker text ("Ingående saldo 2026-05-03"), if any. */ +function markerDate(text: string, dateCell: string): string | null { + const inText = /(\d{4}-\d{2}-\d{2})/.exec(text) + return (inText ? normalizeDate(inText[1]) : null) ?? normalizeDate(dateCell) +} + function splitRow(line: string): string[] { return parseCSVLine(line, ';').map((cell) => cell.trim()) } @@ -136,11 +163,17 @@ export function parseSkattekontoFile( const issues: SkattekontoFileParseIssue[] = [] let companyName: string | null = null let orgNumber: string | null = null - let openingSaldo: number | null = null - let closingSaldo: number | null = null + // A statement can carry several marker pairs (one per year or per page). + // The statement-level check runs from the earliest opening to the latest + // closing; intermediate pairs cancel out. Order by the marker's own date, + // falling back to file order for undated markers. + let opening: { saldo: number; date: string | null; seq: number } | null = null + let closing: { saldo: number; date: string | null; seq: number } | null = null let sawSaldoMarker = false + let markerSeq = 0 let totalRows = 0 let skippedRows = 0 + let unreadableAmountRows = 0 const seenContent = new Map() @@ -163,17 +196,20 @@ export function parseSkattekontoFile( const markerText = cells[1] ?? '' if (OPENING_MARKER_RE.test(markerText) || CLOSING_MARKER_RE.test(markerText)) { sawSaldoMarker = true - const amount = parseAmount(cells[2] ?? '') + const amount = parseMarkerAmount(cells) if (amount === null) { issues.push({ row: i + 1, message: `Kunde inte läsa saldobeloppet: ${cells[2] ?? ''}`, severity: 'warning', }) - } else if (OPENING_MARKER_RE.test(markerText)) { - openingSaldo = amount - } else { - closingSaldo = amount + continue + } + const marker = { saldo: amount, date: markerDate(markerText, cells[0] ?? ''), seq: markerSeq++ } + if (OPENING_MARKER_RE.test(markerText)) { + if (!opening || isEarlierMarker(marker, opening)) opening = marker + } else if (!closing || isEarlierMarker(closing, marker)) { + closing = marker } continue } @@ -206,6 +242,8 @@ export function parseSkattekontoFile( severity: 'warning', }) skippedRows++ + // A dated event we could not read is money missing from the sum check. + unreadableAmountRows++ continue } @@ -223,20 +261,32 @@ export function parseSkattekontoFile( rows.push({ transaktionsdatum: date, transaktionstext: text, belopp, raw_line: line }) } - // Integrity: the statement must sum. A mismatch means a truncated or - // hand-edited file: surfaced as an error so the route refuses the import. - // A file that HAS saldo markers but not both valid balances is equally - // suspect (cut off before "Utgående saldo", or a garbled amount): fail it - // rather than silently skipping the check. Only marker-less legacy files - // legitimately have no balances to check (sum_valid stays null). + // Integrity: a complete statement sums (opening + events = closing). A + // mismatch means a truncated, filtered or hand-edited file, or dated rows + // whose amount we could not read. It is reported as an error-severity + // issue with the figures; the import route no longer refuses the file on + // it (the preview shows the gap and asks the user to confirm), because + // every parsed row is still a real event that is reviewed before booking + // and re-importing a complete file later dedups. A file that HAS saldo + // markers but not both readable balances is flagged the same way. Only + // marker-less legacy files legitimately have nothing to check + // (sum_valid stays null). + const openingSaldo = opening?.saldo ?? null + const closingSaldo = closing?.saldo ?? null let sumValid: boolean | null = null + let eventsSum: number | null = null + let sumDifference: number | null = null if (openingSaldo !== null && closingSaldo !== null) { - const sum = rows.reduce((acc, row) => roundOre(acc + row.belopp), openingSaldo) - sumValid = Math.abs(sum - closingSaldo) < 0.005 + eventsSum = rows.reduce((acc, row) => roundOre(acc + row.belopp), openingSaldo) + sumDifference = roundOre(closingSaldo - eventsSum) + sumValid = Math.abs(sumDifference) < 0.005 if (!sumValid) { issues.push({ row: 0, - message: `Ingående saldo plus transaktioner (${sum}) stämmer inte med utgående saldo (${closingSaldo})`, + message: + unreadableAmountRows > 0 + ? `Ingående saldo plus händelser (${eventsSum}) stämmer inte med utgående saldo (${closingSaldo}); ${unreadableAmountRows} rader med oläsbart belopp saknas i summan` + : `Ingående saldo plus händelser (${eventsSum}) stämmer inte med utgående saldo (${closingSaldo}); differens ${sumDifference}`, severity: 'error', }) } @@ -262,11 +312,23 @@ export function parseSkattekontoFile( opening_saldo: openingSaldo, closing_saldo: closingSaldo, sum_valid: sumValid, + events_sum: eventsSum, + sum_difference: sumDifference, issues, stats: { total_rows: totalRows, parsed_rows: rows.length, skipped_rows: skippedRows, + unreadable_amount_rows: unreadableAmountRows, }, } } + +/** Earlier by marker date when both are dated; otherwise by file order. */ +function isEarlierMarker( + a: { date: string | null; seq: number }, + b: { date: string | null; seq: number }, +): boolean { + if (a.date && b.date && a.date !== b.date) return a.date < b.date + return a.seq < b.seq +} diff --git a/lib/import/skattekonto-file/types.ts b/lib/import/skattekonto-file/types.ts index 8ecc41ab..fea3205b 100644 --- a/lib/import/skattekonto-file/types.ts +++ b/lib/import/skattekonto-file/types.ts @@ -38,14 +38,22 @@ export interface SkattekontoFileParseResult { closing_saldo: number | null /** * opening_saldo + sum(rows) === closing_saldo, checked when both markers - * exist. False means the file is truncated or hand-edited and must not be - * imported silently. Null when the file carries no saldo markers. + * exist. False means the file is truncated, filtered, hand-edited or has + * dated rows we could not read: the preview surfaces the gap and asks the + * user to confirm before importing. Null when the file carries no saldo + * markers. */ sum_valid: boolean | null + /** opening_saldo + sum(rows); null when the check could not run. */ + events_sum: number | null + /** closing_saldo - events_sum; 0 on a consistent statement, null when unchecked. */ + sum_difference: number | null issues: SkattekontoFileParseIssue[] stats: { total_rows: number parsed_rows: number skipped_rows: number + /** Dated rows skipped for an unreadable amount: money missing from the sum check. */ + unreadable_amount_rows: number } } diff --git a/messages/en.json b/messages/en.json index 0ad5b1d6..96600b92 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7116,6 +7116,17 @@ "skattekonto_org_mismatch_title": "The statement belongs to another company", "skattekonto_org_mismatch_body": "The file is a statement for {companyName} ({orgNumber}), which does not match the active company's organisation number. Make sure you have the right company selected before importing.", "skattekonto_org_mismatch_confirm": "I am sure this statement belongs to the active company", + "skattekonto_sum_gap_title": "The statement does not sum", + "skattekonto_sum_gap_opening": "Opening balance", + "skattekonto_sum_gap_events": "Events in the file", + "skattekonto_sum_gap_expected": "Opening balance + events", + "skattekonto_sum_gap_closing": "Closing balance per the file", + "skattekonto_sum_gap_difference": "Difference", + "skattekonto_sum_gap_no_saldo": "The file has no readable opening or closing balance, so the statement cannot be reconciled.", + "skattekonto_sum_gap_body": "Events are missing from the file: it may have been downloaded with a filter, cut off or edited. You can still import the events that are in it; nothing is booked automatically, and importing a complete statement later fills the gap without duplicates.", + "skattekonto_sum_gap_body_unreadable": "{count, plural, one {1 dated row has an amount we could not read and is missing from the sum.} other {# dated rows have amounts we could not read and are missing from the sum.}} You can still import the readable events; nothing is booked automatically. Send us the file and we will teach the import to read it.", + "skattekonto_sum_gap_confirm": "I understand the statement is incomplete and want to import the events it contains", + "skattekonto_after_import_note": "Nothing is booked at import. The events land on the tax account page; those that already have a voucher on 1630 (for example deposits you booked from the bank side) are flagged as possible duplicates and linked to that voucher instead of being booked again.", "skattekonto_duplicates_note": "{count, plural, one {1 event already exists on the tax account and will be skipped.} other {# events already exist on the tax account and will be skipped.}}", "skattekonto_issues_title": "{count, plural, one {1 row could not be fully read} other {# rows could not be fully read}}", "skattekonto_issue_row": "Row {row}", diff --git a/messages/sv.json b/messages/sv.json index f5283e44..bb924432 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -7116,6 +7116,17 @@ "skattekonto_org_mismatch_title": "Utdraget gäller ett annat företag", "skattekonto_org_mismatch_body": "Filen är ett kontoutdrag för {companyName} ({orgNumber}), vilket inte matchar det aktiva företagets organisationsnummer. Kontrollera att du valt rätt företag innan du importerar.", "skattekonto_org_mismatch_confirm": "Jag är säker på att utdraget hör till det aktiva företaget", + "skattekonto_sum_gap_title": "Utdraget summerar inte", + "skattekonto_sum_gap_opening": "Ingående saldo", + "skattekonto_sum_gap_events": "Händelser i filen", + "skattekonto_sum_gap_expected": "Ingående saldo + händelser", + "skattekonto_sum_gap_closing": "Utgående saldo enligt filen", + "skattekonto_sum_gap_difference": "Differens", + "skattekonto_sum_gap_no_saldo": "Filen saknar ett läsbart ingående eller utgående saldo, så utdraget kan inte stämmas av.", + "skattekonto_sum_gap_body": "Händelser saknas i filen: den kan vara nedladdad med ett filter, avklippt eller redigerad. Du kan importera händelserna som finns med ändå; inget bokförs automatiskt, och en senare import av ett komplett utdrag fyller på det som fattas utan dubbletter.", + "skattekonto_sum_gap_body_unreadable": "{count, plural, one {En rad med datum har ett belopp som inte gick att läsa och saknas därför i summan.} other {# rader med datum har belopp som inte gick att läsa och saknas därför i summan.}} Du kan importera de läsbara händelserna ändå; inget bokförs automatiskt. Skicka gärna filen till oss så lär vi importen läsa den.", + "skattekonto_sum_gap_confirm": "Jag förstår att utdraget är ofullständigt och vill importera händelserna som finns", + "skattekonto_after_import_note": "Inget bokförs vid importen. Händelserna hamnar på skattekontosidan; de som redan har ett verifikat på 1630 (till exempel insättningar du bokfört från banken) markeras som möjlig dubblett och kopplas till det verifikatet i stället för att bokföras en gång till.", "skattekonto_duplicates_note": "{count, plural, one {En händelse finns redan på skattekontot och hoppas över.} other {# händelser finns redan på skattekontot och hoppas över.}}", "skattekonto_issues_title": "{count, plural, one {En rad kunde inte läsas fullt ut} other {# rader kunde inte läsas fullt ut}}", "skattekonto_issue_row": "Rad {row}",