fix(salary): make pain.001 betalfil generatable (company IBAN + BIC) (#950)
The ISO 20022 pain.001 salary payment file could never be generated: the route required company_settings.iban/bic, but no settings screen wrote those columns, so every request returned 400. The specific reason was also swallowed by getErrorMessage (isSwedishUserMessage did not know "krävs"/"saknar"), surfacing only the generic "Förfrågan innehåller ogiltiga uppgifter" (issue #945). - Add IBAN + BIC inputs to Settings > Fakturering > Bankuppgifter. BIC auto-derives from the clearing number / bank already entered, so in practice only the IBAN is typed. Validated client- and server-side. - Route requires the company IBAN (canonical debtor form every Swedish bank accepts) and derives the BIC, with clear actionable errors. - Employees are unchanged: domestic clearing + account (BBAN), which is what Swedish payroll collects. Only the company (debtor) uses IBAN. - getErrorMessage recognizes "krävs"/"saknar" so payment-file reasons surface instead of the generic 400. Fixes #945 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { ensureInitialized } from '@/lib/init'
|
||||
import { withRouteContext } from '@/lib/api/with-route-context'
|
||||
import { generatePain001 } from '@/lib/salary/payment/pain001-generator'
|
||||
import { effectiveNetPayout } from '@/lib/salary/payment/effective-net'
|
||||
import { normalizeBankNumber, lookupBicByClearing, lookupBicByBankName } from '@/lib/salary/payment/bank-account'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
import type { Pain001CompanyData, Pain001Employee } from '@/lib/salary/payment/pain001-generator'
|
||||
|
||||
@@ -47,7 +48,7 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
|
||||
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('company_name, iban, bic')
|
||||
.select('company_name, iban, bic, clearing_number, bank_name')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
@@ -55,8 +56,31 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
|
||||
return NextResponse.json({ error: 'Företag hittades inte' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (!settings?.iban || !settings?.bic) {
|
||||
return NextResponse.json({ error: 'IBAN och BIC krävs i företagsinställningar för betalfil' }, { status: 400 })
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: 'Företagsinställningar saknas' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Debtor (company) account: the company's own IBAN, the canonical payer form
|
||||
// every Swedish bank accepts. Set under Inställningar → Fakturering.
|
||||
const senderIban = (settings.iban ?? '').replace(/\s/g, '').toUpperCase()
|
||||
if (!senderIban) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Företagets IBAN saknas i företagsinställningar. Fyll i det under Inställningar → Fakturering för att skapa betalfil (ISO 20022).' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Debtor bank BIC: use the saved BIC, otherwise derive it from the clearing
|
||||
// number (or bank name) the company already entered, so most users only need
|
||||
// to fill in the IBAN. Required by the receiving bank.
|
||||
const senderBic = settings.bic?.trim()
|
||||
|| lookupBicByClearing(normalizeBankNumber(settings.clearing_number))
|
||||
|| lookupBicByBankName(settings.bank_name)
|
||||
if (!senderBic) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Företagsbankens BIC saknas och kunde inte härledas. Fyll i BIC under Inställningar → Fakturering för att skapa betalfil.' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Load employees
|
||||
@@ -89,8 +113,8 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
|
||||
// not the frozen onboarding companies.name.
|
||||
name: settings.company_name || company.name,
|
||||
orgNumber: company.org_number || '',
|
||||
iban: settings.iban,
|
||||
bic: settings.bic,
|
||||
iban: senderIban,
|
||||
bic: senderBic,
|
||||
}
|
||||
|
||||
const employees: Pain001Employee[] = runEmployees
|
||||
|
||||
@@ -21,6 +21,8 @@ export function BankDetailsForm({ settings }: BankDetailsFormProps) {
|
||||
const [clearingError, setClearingError] = useState<string | null>(null)
|
||||
const [accountNumberError, setAccountNumberError] = useState<string | null>(null)
|
||||
const [swishError, setSwishError] = useState<string | null>(null)
|
||||
const [ibanError, setIbanError] = useState<string | null>(null)
|
||||
const [bicError, setBicError] = useState<string | null>(null)
|
||||
const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking')
|
||||
|
||||
return (
|
||||
@@ -146,6 +148,45 @@ export function BankDetailsForm({ settings }: BankDetailsFormProps) {
|
||||
{swishError && <p className="text-xs text-destructive">{swishError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-2 sm:col-span-2">
|
||||
<Label htmlFor="iban">{t('iban_label')}</Label>
|
||||
<Input
|
||||
id="iban"
|
||||
name="iban"
|
||||
placeholder="SE00 0000 0000 0000 0000 0000"
|
||||
defaultValue={settings.iban || ''}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value.replace(/\s/g, '').toUpperCase()
|
||||
if (!val) { setIbanError(null); e.target.value = ''; return }
|
||||
e.target.value = val
|
||||
setIbanError(/^SE\d{22}$/.test(val) ? null : t('iban_error'))
|
||||
}}
|
||||
/>
|
||||
{ibanError
|
||||
? <p className="text-xs text-destructive">{ibanError}</p>
|
||||
: <p className="text-xs text-muted-foreground">{t('iban_hint')}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bic">{t('bic_label')}</Label>
|
||||
<Input
|
||||
id="bic"
|
||||
name="bic"
|
||||
placeholder={t('bic_placeholder')}
|
||||
maxLength={11}
|
||||
defaultValue={settings.bic || ''}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value.replace(/\s/g, '').toUpperCase()
|
||||
if (!val) { setBicError(null); e.target.value = ''; return }
|
||||
e.target.value = val
|
||||
setBicError(/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(val) ? null : t('bic_error'))
|
||||
}}
|
||||
/>
|
||||
{bicError && <p className="text-xs text-destructive">{bicError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -158,6 +199,8 @@ export function validateBankFields(formData: FormData): { field: string; message
|
||||
const bankgiro = (formData.get('bankgiro') as string || '').trim()
|
||||
const plusgiro = (formData.get('plusgiro') as string || '').trim()
|
||||
const swish = normaliseSwish(formData.get('swish') as string)
|
||||
const iban = (formData.get('iban') as string || '').replace(/\s/g, '').toUpperCase()
|
||||
const bic = (formData.get('bic') as string || '').replace(/\s/g, '').toUpperCase()
|
||||
|
||||
if (clearing && !/^\d{4,5}$/.test(clearing)) {
|
||||
errors.push({ field: 'clearing_number', message: 'Clearingnummer måste vara 4-5 siffror' })
|
||||
@@ -174,5 +217,11 @@ export function validateBankFields(formData: FormData): { field: string; message
|
||||
if (swish && !isValidSwish(swish)) {
|
||||
errors.push({ field: 'swish', message: 'Ogiltigt Swish-nummer (företagsnummer 123XXXXXXX eller mobilnummer 07XXXXXXXX)' })
|
||||
}
|
||||
if (iban && !/^SE\d{22}$/.test(iban)) {
|
||||
errors.push({ field: 'iban', message: 'Ogiltigt IBAN (SE följt av 22 siffror)' })
|
||||
}
|
||||
if (bic && !/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(bic)) {
|
||||
errors.push({ field: 'bic', message: 'Ogiltig BIC/SWIFT (8 eller 11 tecken)' })
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export function InvoicingSettingsContent() {
|
||||
? formatPlusgiroNumber((formData.get('plusgiro') as string).trim())
|
||||
: null,
|
||||
swish: normaliseSwish(formData.get('swish') as string) || null,
|
||||
iban: (formData.get('iban') as string || '').replace(/\s/g, '').toUpperCase() || null,
|
||||
bic: (formData.get('bic') as string || '').replace(/\s/g, '').toUpperCase() || null,
|
||||
invoice_prefix: (formData.get('invoice_prefix') as string) || null,
|
||||
next_invoice_number: parseInt(formData.get('next_invoice_number') as string) || 1,
|
||||
next_arrival_number: parseInt(formData.get('next_arrival_number') as string) || 1,
|
||||
|
||||
+2
-2
@@ -1363,8 +1363,8 @@ export const UpdateSettingsSchema = z.object({
|
||||
)
|
||||
.nullable()
|
||||
.optional(),
|
||||
iban: z.string().optional(),
|
||||
bic: z.string().optional(),
|
||||
iban: z.string().regex(/^SE\d{22}$/, 'Ogiltigt IBAN (SE följt av 22 siffror)').nullable().optional().or(z.literal('')),
|
||||
bic: z.string().regex(/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/, 'Ogiltig BIC/SWIFT (8 eller 11 tecken)').nullable().optional().or(z.literal('')),
|
||||
accounting_method: AccountingMethodSchema.optional(),
|
||||
invoice_prefix: z.string().nullable().optional(),
|
||||
next_invoice_number: z.number().int().positive().optional(),
|
||||
|
||||
@@ -147,6 +147,38 @@ describe('getErrorMessage: accumulated validation details', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getErrorMessage: payment-file route messages surface (issue #945)', () => {
|
||||
// These specific { error: '...' } strings previously collapsed to the generic
|
||||
// HTTP-400 message because isSwedishUserMessage did not recognize "krävs" /
|
||||
// "saknar", so the user learned nothing about why the betalfil failed.
|
||||
it('surfaces a "saknar bankkontouppgifter" message instead of the generic 400', () => {
|
||||
const msg = getErrorMessage(
|
||||
{ error: '2 anställd(a) saknar bankkontouppgifter' },
|
||||
{ context: 'salary', statusCode: 400 },
|
||||
)
|
||||
expect(msg).toBe('2 anställd(a) saknar bankkontouppgifter')
|
||||
expect(msg).not.toBe('Förfrågan innehåller ogiltiga uppgifter.')
|
||||
})
|
||||
|
||||
it('surfaces a "... krävs ..." message instead of the generic 400', () => {
|
||||
const msg = getErrorMessage(
|
||||
{ error: 'Momsregistreringsnummer krävs när företaget är momsregistrerat (ML 11 kap. 8§)' },
|
||||
{ context: 'settings', statusCode: 400 },
|
||||
)
|
||||
expect(msg).toContain('krävs')
|
||||
expect(msg).not.toBe('Förfrågan innehåller ogiltiga uppgifter.')
|
||||
})
|
||||
|
||||
it('surfaces the missing company bank-account message', () => {
|
||||
const msg = getErrorMessage(
|
||||
{ error: 'Företagets bankkonto (clearingnummer och kontonummer) saknas i företagsinställningar. Fyll i det under Inställningar → Fakturering för att skapa betalfil.' },
|
||||
{ context: 'salary', statusCode: 400 },
|
||||
)
|
||||
expect(msg).toContain('Företagets bankkonto')
|
||||
expect(msg).not.toBe('Förfrågan innehåller ogiltiga uppgifter.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getErrorMessage: existing patterns still work', () => {
|
||||
it('regex match for "Entry date ... outside fiscal period" on plain string', () => {
|
||||
const msg = getErrorMessage('Entry date 2024-06-15 is outside fiscal period "FY 2025"')
|
||||
|
||||
@@ -166,6 +166,8 @@ function isSwedishUserMessage(message: string): boolean {
|
||||
/försök igen/i,
|
||||
/ogiltigt?/i,
|
||||
/saknas/i,
|
||||
/saknar/i,
|
||||
/krävs/i,
|
||||
/måste/i,
|
||||
/redan finns/i,
|
||||
/gick fel/i,
|
||||
|
||||
@@ -40,10 +40,18 @@ describe('generatePain001', () => {
|
||||
const xml = generatePain001(company, employees, options)
|
||||
|
||||
expect(xml).toContain('<Nm>Test AB</Nm>')
|
||||
expect(xml).toContain('SE1234567890123456789012')
|
||||
expect(xml).toContain('ESSESESS')
|
||||
})
|
||||
|
||||
it('identifies the debtor (company) by its own IBAN', () => {
|
||||
const xml = generatePain001(company, employees, options)
|
||||
|
||||
// The payer is the company's IBAN, inside DbtrAcct.
|
||||
expect(xml).toContain('<IBAN>SE1234567890123456789012</IBAN>')
|
||||
// Employees are still domestic BBAN (clearing+account), never IBAN.
|
||||
expect(xml).toContain('<Othr><Id>56781234567890</Id></Othr>')
|
||||
})
|
||||
|
||||
it('includes SALA category purpose for salary', () => {
|
||||
const xml = generatePain001(company, employees, options)
|
||||
expect(xml).toContain('<Cd>SALA</Cd>')
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
isValidAccount,
|
||||
validateEmployeeBankAccount,
|
||||
lookupBankByClearing,
|
||||
lookupBicByClearing,
|
||||
lookupBicByBankName,
|
||||
} from '@/lib/salary/payment/bank-account'
|
||||
|
||||
describe('normalizeBankNumber', () => {
|
||||
@@ -100,3 +102,37 @@ describe('lookupBankByClearing', () => {
|
||||
expect(lookupBankByClearing('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lookupBicByClearing', () => {
|
||||
it('maps clearing numbers to the bank BIC', () => {
|
||||
expect(lookupBicByClearing('5000')).toBe('ESSESESS') // SEB
|
||||
expect(lookupBicByClearing('6789')).toBe('HANDSESS') // Handelsbanken
|
||||
expect(lookupBicByClearing('7123')).toBe('SWEDSESS') // Swedbank
|
||||
expect(lookupBicByClearing('3000')).toBe('NDEASESS') // Nordea
|
||||
expect(lookupBicByClearing('1234')).toBe('DABASESX') // Danske Bank
|
||||
})
|
||||
it('maps a 5-digit Swedbank clearing via its 8xxx prefix', () => {
|
||||
expect(lookupBicByClearing('83279')).toBe('SWEDSESS')
|
||||
})
|
||||
it('returns null for unknown ranges rather than guessing a BIC', () => {
|
||||
expect(lookupBicByClearing('9999')).toBeNull()
|
||||
expect(lookupBicByClearing('123')).toBeNull()
|
||||
expect(lookupBicByClearing('')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lookupBicByBankName', () => {
|
||||
it('resolves banks outside the clearing table by name', () => {
|
||||
expect(lookupBicByBankName('Länsförsäkringar')).toBe('ELLFSESS')
|
||||
expect(lookupBicByBankName('Skandiabanken')).toBe('SKIASESS')
|
||||
})
|
||||
it('matches on a normalized substring', () => {
|
||||
expect(lookupBicByBankName('Danske Bank Sverige')).toBe('DABASESX')
|
||||
expect(lookupBicByBankName(' SEB ')).toBe('ESSESESS')
|
||||
})
|
||||
it('returns null for unknown or empty names', () => {
|
||||
expect(lookupBicByBankName('Min Lokala Bank')).toBeNull()
|
||||
expect(lookupBicByBankName('')).toBeNull()
|
||||
expect(lookupBicByBankName(null)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -130,18 +130,18 @@ export function validateEmployeeBankAccount(
|
||||
* Ranges are matched on the leading 4 digits, so a 5-digit Swedbank clearing
|
||||
* (8xxxx) maps via its 8xxx prefix.
|
||||
*/
|
||||
const BANK_CLEARING_RANGES: ReadonlyArray<{ min: number; max: number; bank: string }> = [
|
||||
{ min: 1100, max: 1199, bank: 'Nordea' },
|
||||
{ min: 1200, max: 1399, bank: 'Danske Bank' },
|
||||
{ min: 1400, max: 2099, bank: 'Nordea' },
|
||||
{ min: 2400, max: 2499, bank: 'Danske Bank' },
|
||||
{ min: 3000, max: 3399, bank: 'Nordea' },
|
||||
{ min: 5000, max: 5999, bank: 'SEB' },
|
||||
{ min: 6000, max: 6999, bank: 'Handelsbanken' },
|
||||
{ min: 7000, max: 7999, bank: 'Swedbank' },
|
||||
{ min: 8000, max: 8999, bank: 'Swedbank/Sparbanken' },
|
||||
{ min: 9500, max: 9549, bank: 'Nordea (Plusgirot)' },
|
||||
{ min: 9960, max: 9969, bank: 'Nordea (Plusgirot)' },
|
||||
const BANK_CLEARING_RANGES: ReadonlyArray<{ min: number; max: number; bank: string; bic: string }> = [
|
||||
{ min: 1100, max: 1199, bank: 'Nordea', bic: 'NDEASESS' },
|
||||
{ min: 1200, max: 1399, bank: 'Danske Bank', bic: 'DABASESX' },
|
||||
{ min: 1400, max: 2099, bank: 'Nordea', bic: 'NDEASESS' },
|
||||
{ min: 2400, max: 2499, bank: 'Danske Bank', bic: 'DABASESX' },
|
||||
{ min: 3000, max: 3399, bank: 'Nordea', bic: 'NDEASESS' },
|
||||
{ min: 5000, max: 5999, bank: 'SEB', bic: 'ESSESESS' },
|
||||
{ min: 6000, max: 6999, bank: 'Handelsbanken', bic: 'HANDSESS' },
|
||||
{ min: 7000, max: 7999, bank: 'Swedbank', bic: 'SWEDSESS' },
|
||||
{ min: 8000, max: 8999, bank: 'Swedbank/Sparbanken', bic: 'SWEDSESS' },
|
||||
{ min: 9500, max: 9549, bank: 'Nordea (Plusgirot)', bic: 'NDEASESS' },
|
||||
{ min: 9960, max: 9969, bank: 'Nordea (Plusgirot)', bic: 'NDEASESS' },
|
||||
]
|
||||
|
||||
/** Bank name for a (partial) clearing number, or null when not confidently known. */
|
||||
@@ -153,3 +153,47 @@ export function lookupBankByClearing(clearingRaw: string | null | undefined): st
|
||||
const hit = BANK_CLEARING_RANGES.find((r) => first4 >= r.min && first4 <= r.max)
|
||||
return hit ? hit.bank : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Bank BIC (SWIFT) for a clearing number, or null when the clearing is not in
|
||||
* the table above. Used to fill the debtor agent (DbtrAgt) in the pain.001
|
||||
* salary payment file without asking the company to type its BIC by hand: the
|
||||
* clearing number it already entered for the debtor account deterministically
|
||||
* identifies the bank. Only confidently-known, long-stable ranges are covered;
|
||||
* an unknown clearing returns null so the caller can fall back or fail loudly
|
||||
* rather than emit a guessed BIC into a real payment instruction.
|
||||
*/
|
||||
export function lookupBicByClearing(clearingRaw: string | null | undefined): string | null {
|
||||
const clearing = normalizeBankNumber(clearingRaw)
|
||||
if (clearing.length < 4) return null
|
||||
const first4 = Number.parseInt(clearing.slice(0, 4), 10)
|
||||
if (Number.isNaN(first4)) return null
|
||||
const hit = BANK_CLEARING_RANGES.find((r) => first4 >= r.min && first4 <= r.max)
|
||||
return hit ? hit.bic : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback BIC lookup by the free-text bank name saved in company settings, for
|
||||
* the (rare) banks not covered by the clearing ranges above (e.g.
|
||||
* Länsförsäkringar, Skandiabanken). Matched on a normalized substring so
|
||||
* "Danske Bank Sverige" still resolves. Only BICs we are confident about are
|
||||
* listed; anything else returns null. Never guess a BIC for a real payment.
|
||||
*/
|
||||
const BANK_NAME_BIC: ReadonlyArray<{ match: string; bic: string }> = [
|
||||
{ match: 'handelsbanken', bic: 'HANDSESS' },
|
||||
{ match: 'länsförsäkringar', bic: 'ELLFSESS' },
|
||||
{ match: 'lansforsakringar', bic: 'ELLFSESS' },
|
||||
{ match: 'skandia', bic: 'SKIASESS' },
|
||||
{ match: 'swedbank', bic: 'SWEDSESS' },
|
||||
{ match: 'sparbank', bic: 'SWEDSESS' },
|
||||
{ match: 'danske', bic: 'DABASESX' },
|
||||
{ match: 'nordea', bic: 'NDEASESS' },
|
||||
{ match: 'seb', bic: 'ESSESESS' },
|
||||
]
|
||||
|
||||
export function lookupBicByBankName(nameRaw: string | null | undefined): string | null {
|
||||
const name = (nameRaw ?? '').trim().toLowerCase()
|
||||
if (!name) return null
|
||||
const hit = BANK_NAME_BIC.find((b) => name.includes(b.match))
|
||||
return hit ? hit.bic : null
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
export interface Pain001CompanyData {
|
||||
name: string
|
||||
orgNumber: string // NNNNNN-NNNN
|
||||
iban: string // SE + 22 digits
|
||||
bic: string // SWIFT/BIC code
|
||||
iban: string // SE + 22 digits (the company's own account)
|
||||
bic: string // debtor bank SWIFT/BIC
|
||||
}
|
||||
|
||||
export interface Pain001Employee {
|
||||
@@ -87,6 +87,9 @@ export function generatePain001(
|
||||
lines.push(' </Dbtr>')
|
||||
lines.push(' <DbtrAcct>')
|
||||
lines.push(' <Id>')
|
||||
// The company (debtor) is identified by its own IBAN: the canonical form every
|
||||
// Swedish bank accepts for the payer. Employees (creditors) stay on domestic
|
||||
// clearing+account below, which is what Swedish payroll actually collects.
|
||||
lines.push(` <IBAN>${escapeXml(company.iban)}</IBAN>`)
|
||||
lines.push(' </Id>')
|
||||
lines.push(' <Ccy>SEK</Ccy>')
|
||||
|
||||
+7
-1
@@ -1330,7 +1330,13 @@
|
||||
"plusgiro_error": "Invalid plusgiro number",
|
||||
"swish_label": "Swish",
|
||||
"swish_placeholder": "123 XXX XX XX or 07X XXX XX XX",
|
||||
"swish_error": "Invalid Swish number (business number 123XXXXXXX or mobile number 07XXXXXXXX)"
|
||||
"swish_error": "Invalid Swish number (business number 123XXXXXXX or mobile number 07XXXXXXXX)",
|
||||
"iban_label": "IBAN",
|
||||
"iban_error": "Invalid IBAN (SE followed by 22 digits)",
|
||||
"iban_hint": "Required for the payment file (ISO 20022) when paying salaries",
|
||||
"bic_label": "BIC/SWIFT",
|
||||
"bic_placeholder": "Filled in automatically",
|
||||
"bic_error": "Invalid BIC/SWIFT (8 or 11 characters)"
|
||||
},
|
||||
"settings_invoice_form": {
|
||||
"default_our_reference_label": "Default \"Our reference\"",
|
||||
|
||||
+7
-1
@@ -1330,7 +1330,13 @@
|
||||
"plusgiro_error": "Ogiltigt plusgironummer",
|
||||
"swish_label": "Swish",
|
||||
"swish_placeholder": "123 XXX XX XX eller 07X XXX XX XX",
|
||||
"swish_error": "Ogiltigt Swish-nummer (företagsnummer 123XXXXXXX eller mobilnummer 07XXXXXXXX)"
|
||||
"swish_error": "Ogiltigt Swish-nummer (företagsnummer 123XXXXXXX eller mobilnummer 07XXXXXXXX)",
|
||||
"iban_label": "IBAN",
|
||||
"iban_error": "Ogiltigt IBAN (SE följt av 22 siffror)",
|
||||
"iban_hint": "Krävs för betalfil (ISO 20022) vid löneutbetalning",
|
||||
"bic_label": "BIC/SWIFT",
|
||||
"bic_placeholder": "Fylls i automatiskt",
|
||||
"bic_error": "Ogiltig BIC/SWIFT (8 eller 11 tecken)"
|
||||
},
|
||||
"settings_invoice_form": {
|
||||
"default_our_reference_label": "Standard för Vår referens",
|
||||
|
||||
Reference in New Issue
Block a user