fix(import): preserve customized SIE #KONTO account names (#669)
* feat(import): add syncMappedAccounts helper for account create + rename Single home for the create-missing-accounts logic that exists in three near-identical copies (executeSIEImport, the SIE execute route, and the arcim-migration extension), plus a new rename pass that carries customized SIE #KONTO names into accounts that already exist (e.g. K1-seeded defaults). The file's name applies only to identity mappings (source === target); remapped targets keep their BAS/current name. With updateAccountNames=false the behavior matches the legacy code exactly. Not wired up yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): preserve SIE #KONTO account names; add updateAccountNames option Customer report: account names customized in Fortnox did not follow into Accounted via SIE import. The import always used BAS default names for accounts in the BAS reference and never touched accounts that already existed (the K1-seeded chart), so the file's names were silently dropped. executeSIEImport now routes account creation through syncMappedAccounts, which prefers the file's #KONTO name for identity-mapped accounts and renames existing accounts whose name differs (surfaced as a warning). New option updateAccountNames (default true) restores the old behavior when disabled. The duplicated pre-create blocks in the execute route and the arcim-migration extension are removed — executeSIEImport owns account sync on every path now, including the Fortnox re-sync (idempotent renames). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(mcp): expose update_account_names on gnubok_import_sie Optional boolean on the tool schema, staged into the pending operation and threaded through commitImportSie to executeSIEImport. Defaults to true at both stage and commit time — the commit-side default also covers operations staged before the param existed (Boolean(undefined) would have silently flipped it off). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): v1 SIE import generated no account mappings The route passed [] as mappings to executeSIEImport, which the mapping-coverage guard (added in #613) rejects for any real file — and before that guard, every voucher was silently skipped as unmapped. The route has never produced a working import for files with vouchers. Generate mappings server-side from the file's #KONTO records plus stored per-company overrides (same as the dashboard execute route), reject unmappable files with a clean 400 before the operation row is created, and expose options.updateAccountNames (default true). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(import): "Använd kontonamn från filen" toggle in import review step New switch (default on) controlling whether the SIE file's #KONTO names are carried into the chart of accounts. Helper text shows how many identity-mapped accounts carry names that differ from the BAS defaults. The page already serializes the whole options object to the execute route, so no further wiring is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(import): address PR #669 review — parallel renames, rename audit trail - Rename pass now runs UPDATEs concurrently in bounded batches of 25 (greptile P2): a re-sync with many custom names no longer serializes N round trips, and a pathological full-chart rename cannot stampede the API. Per-rename failures stay non-fatal via Promise.allSettled. - Persist the per-account rename detail (number, from, to) into sie_imports.migration_documentation as accountRenames — the behandlingshistorik record per BFNAR 2013:2 (swedish-compliance review); the result warnings only carry the count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4a54467599
commit
f7cd1b86e7
+39
-66
@@ -18,6 +18,7 @@ import type {
|
||||
} from './types'
|
||||
import type { CreateJournalEntryLineInput } from '@/types'
|
||||
import { mappingsToMap, getMappingStats } from './account-mapper'
|
||||
import { syncMappedAccounts } from './account-sync'
|
||||
import { calculateFileHash } from './sie-parser'
|
||||
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
||||
import { classifyAccount } from '@/lib/bookkeeping/account-classifier'
|
||||
@@ -1807,6 +1808,12 @@ export async function loadMappings(supabase: SupabaseClient, companyId: string):
|
||||
* Replace only cancels journal entries with source_type='import' — entries
|
||||
* the user created natively in Accounted (categorized transactions, invoices,
|
||||
* etc.) are left alone. See the replace_sie_import RPC.
|
||||
*
|
||||
* `updateAccountNames` (default true) carries the SIE file's #KONTO names
|
||||
* into the chart for identity-mapped accounts: new accounts are created with
|
||||
* the file's name and existing accounts whose name differs are renamed.
|
||||
* When false, accounts are created with BAS default names and existing
|
||||
* accounts are left untouched (the pre-2026-06 behavior).
|
||||
*/
|
||||
export async function executeSIEImport(
|
||||
supabase: SupabaseClient,
|
||||
@@ -1822,6 +1829,7 @@ export async function executeSIEImport(
|
||||
importTransactions: boolean
|
||||
voucherSeries?: string
|
||||
onExistingPeriod?: 'block' | 'replace'
|
||||
updateAccountNames?: boolean
|
||||
}
|
||||
): Promise<ImportResult> {
|
||||
const result: ImportResult = {
|
||||
@@ -1837,6 +1845,7 @@ export async function executeSIEImport(
|
||||
}
|
||||
|
||||
const onExistingPeriod = options.onExistingPeriod ?? 'block'
|
||||
const updateAccountNames = options.updateAccountNames ?? true
|
||||
|
||||
try {
|
||||
// Validate all accounts are mapped
|
||||
@@ -1950,72 +1959,32 @@ export async function executeSIEImport(
|
||||
// Build account mapping lookup
|
||||
const accountMap = mappingsToMap(mappings)
|
||||
|
||||
// Ensure all mapped target accounts exist in chart_of_accounts.
|
||||
// Uses a single batch query + batch insert instead of per-account round trips.
|
||||
const targetAccounts = [...new Set(
|
||||
mappings.filter(m => m.targetAccount).map(m => m.targetAccount!)
|
||||
)]
|
||||
|
||||
if (targetAccounts.length > 0) {
|
||||
const { data: existing } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('account_number')
|
||||
.eq('company_id', companyId)
|
||||
.in('account_number', targetAccounts)
|
||||
|
||||
const existingSet = new Set((existing || []).map(a => a.account_number))
|
||||
const missing = targetAccounts.filter(num => !existingSet.has(num))
|
||||
|
||||
if (missing.length > 0) {
|
||||
const targetNameMap = new Map<string, string>()
|
||||
for (const m of mappings) {
|
||||
if (m.targetAccount) targetNameMap.set(m.targetAccount, m.targetName || m.sourceName)
|
||||
}
|
||||
|
||||
const inserts = missing.map(num => {
|
||||
const basRef = getBASReference(num)
|
||||
if (basRef) {
|
||||
return {
|
||||
user_id: userId,
|
||||
company_id: companyId,
|
||||
account_number: num,
|
||||
account_name: basRef.account_name,
|
||||
account_class: basRef.account_class,
|
||||
account_group: basRef.account_group,
|
||||
account_type: basRef.account_type,
|
||||
normal_balance: basRef.normal_balance,
|
||||
sru_code: basRef.sru_code ?? computeSRUCode(num),
|
||||
k2_excluded: basRef.k2_excluded,
|
||||
plan_type: 'full_bas' as const,
|
||||
is_active: true,
|
||||
is_system_account: false,
|
||||
}
|
||||
}
|
||||
const classNum = parseInt(num.charAt(0), 10)
|
||||
const group = num.substring(0, 2)
|
||||
const classified = classifyAccount(num)
|
||||
return {
|
||||
user_id: userId,
|
||||
company_id: companyId,
|
||||
account_number: num,
|
||||
account_name: targetNameMap.get(num) || `Konto ${num}`,
|
||||
account_class: classNum,
|
||||
account_group: group,
|
||||
account_type: classified.account_type,
|
||||
normal_balance: classified.normal_balance,
|
||||
sru_code: computeSRUCode(num),
|
||||
plan_type: 'full_bas' as const,
|
||||
is_active: true,
|
||||
is_system_account: false,
|
||||
}
|
||||
})
|
||||
|
||||
const { error: insertError } = await supabase.from('chart_of_accounts').insert(inserts)
|
||||
if (insertError && !insertError.message.includes('duplicate')) {
|
||||
result.errors.push(`Failed to create accounts: ${insertError.message}`)
|
||||
return result
|
||||
}
|
||||
}
|
||||
// Ensure all mapped target accounts exist in chart_of_accounts and,
|
||||
// unless disabled, carry the SIE file's #KONTO names into the chart —
|
||||
// customized names from the source system (e.g. Fortnox) would otherwise
|
||||
// be lost to the BAS defaults.
|
||||
const accountSync = await syncMappedAccounts(
|
||||
supabase,
|
||||
companyId,
|
||||
userId,
|
||||
mappings,
|
||||
updateAccountNames
|
||||
)
|
||||
if (accountSync.error) {
|
||||
result.errors.push(`Failed to create accounts: ${accountSync.error}`)
|
||||
return result
|
||||
}
|
||||
if (accountSync.renamed > 0) {
|
||||
result.warnings.push(
|
||||
accountSync.renamed === 1
|
||||
? '1 konto bytte namn till namnet från SIE-filen'
|
||||
: `${accountSync.renamed} konton bytte namn till namnen från SIE-filen`
|
||||
)
|
||||
}
|
||||
if (accountSync.renameFailed > 0) {
|
||||
result.warnings.push(
|
||||
`${accountSync.renameFailed} kontonamn kunde inte uppdateras från SIE-filen`
|
||||
)
|
||||
}
|
||||
|
||||
// Create or find fiscal period
|
||||
@@ -2418,6 +2387,10 @@ export async function executeSIEImport(
|
||||
manual: mappingStats.manual,
|
||||
unmapped: mappingStats.unmapped,
|
||||
},
|
||||
// Behandlingshistorik for #KONTO renames applied by this import
|
||||
// (BFNAR 2013:2 — the warnings array only carries the count).
|
||||
accountRenames:
|
||||
accountSync.renamedAccounts.length > 0 ? accountSync.renamedAccounts : undefined,
|
||||
vouchers: voucherStats,
|
||||
openingBalanceRounding: ibRoundingAdjustment !== 0 ? ibRoundingAdjustment : null,
|
||||
migrationAdjustment: migrationAdjustmentInfo,
|
||||
|
||||
Reference in New Issue
Block a user