Files
accounted/lib/import/customers/column-detector.ts
T
Mattsson 81e9dd224e Add/csv import options (#420)
* feat(import): add customer and supplier parsing functionality

- Implemented customer file parsing in `lib/import/customers/parser.ts` with support for Excel and CSV formats.
- Created types for detected customer columns and parsed customer rows in `lib/import/customers/types.ts`.
- Added tests for customer classification logic in `lib/import/shared/__tests__/classify.test.ts`.
- Developed classification functions for customers and suppliers in `lib/import/shared/classify.ts`.
- Introduced shared column utility functions in `lib/import/shared/column-utils.ts`.
- Implemented supplier file parsing in `lib/import/suppliers/parser.ts` with validation for various fields.
- Created types for detected supplier columns and parsed supplier rows in `lib/import/suppliers/types.ts`.
- Added tests for supplier column detection and parsing in `lib/import/suppliers/__tests__/column-detector.test.ts` and `lib/import/suppliers/__tests__/parser.test.ts`.

* fix(labels): update 'Svenskt företag' to 'Svenskt företag eller organisation' for clarity

* feat(import): refactor encoding handling for Swedish files and add tests for character preservation

* feat(recapt): implement clearRecaptIdentity function and integrate into logout flow

* feat(bookkeeping): implement copy functionality and next voucher sequence retrieval

* feat(import): enhance customer and supplier import functionality with normalization and event handling
2026-05-08 15:42:06 +02:00

107 lines
3.5 KiB
TypeScript

import { findColumn } from '../shared/column-utils'
import type { DetectedCustomerColumns } from './types'
const NAME_KEYWORDS = [
'kundnamn', 'kund namn', 'namn', 'name', 'kund', 'customer', 'customer name',
'företag', 'foretag', 'company', 'företagsnamn', 'foretagsnamn',
]
const ORG_NUMBER_KEYWORDS = [
'orgnr', 'org nr', 'organisationsnummer', 'organisationsnr', 'org',
'personnr', 'personnummer', 'org number', 'organization number',
]
const CUSTOMER_TYPE_KEYWORDS = [
'kundtyp', 'kund typ', 'typ', 'type', 'customer type', 'customer_type',
]
const EMAIL_KEYWORDS = [
'epost', 'e post', 'email', 'mail', 'e mail', 'e-post',
]
const PHONE_KEYWORDS = [
'telefon', 'tel', 'phone', 'mobil', 'mobile', 'telefonnummer',
]
const ADDRESS_LINE1_KEYWORDS = [
'adress', 'address', 'gatuadress', 'street', 'gata',
'address line 1', 'address1', 'adressrad 1',
]
const ADDRESS_LINE2_KEYWORDS = [
'address line 2', 'address2', 'adressrad 2', 'c o', 'co',
]
const POSTAL_CODE_KEYWORDS = [
'postnr', 'postnummer', 'postal code', 'postal_code', 'zip', 'zip code',
]
const CITY_KEYWORDS = ['ort', 'stad', 'city', 'postort']
const COUNTRY_KEYWORDS = ['land', 'country']
const VAT_NUMBER_KEYWORDS = [
'vat', 'vatnr', 'vat nr', 'vat number', 'momsnummer', 'momsregistreringsnummer',
'momsregnr', 'moms nr',
]
const PAYMENT_TERMS_KEYWORDS = [
'betalningsvillkor', 'betalvillkor', 'payment terms', 'kredittid', 'kreditdagar',
'dagar', 'förfallodagar', 'forfallodagar',
]
const NOTES_KEYWORDS = [
'anteckning', 'anteckningar', 'notes', 'kommentar', 'kommentarer', 'comment',
'note', 'beskrivning',
]
/**
* Detect customer-register columns from headers.
* Header-only matching: register imports always have headers, and the column
* structure varies too much to do data-driven fallbacks reliably.
*/
export function detectCustomerColumns(headers: string[]): DetectedCustomerColumns {
const taken = new Set<number>()
const name_col = findColumn(headers, NAME_KEYWORDS, taken) ?? -1
const org_number_col = findColumn(headers, ORG_NUMBER_KEYWORDS, taken)
const customer_type_col = findColumn(headers, CUSTOMER_TYPE_KEYWORDS, taken)
const email_col = findColumn(headers, EMAIL_KEYWORDS, taken)
const phone_col = findColumn(headers, PHONE_KEYWORDS, taken)
const address_line1_col = findColumn(headers, ADDRESS_LINE1_KEYWORDS, taken)
const address_line2_col = findColumn(headers, ADDRESS_LINE2_KEYWORDS, taken)
const postal_code_col = findColumn(headers, POSTAL_CODE_KEYWORDS, taken)
const city_col = findColumn(headers, CITY_KEYWORDS, taken)
const country_col = findColumn(headers, COUNTRY_KEYWORDS, taken)
const vat_number_col = findColumn(headers, VAT_NUMBER_KEYWORDS, taken)
const payment_terms_col = findColumn(headers, PAYMENT_TERMS_KEYWORDS, taken)
const notes_col = findColumn(headers, NOTES_KEYWORDS, taken)
// Confidence: name is required; bonus from how many other columns matched.
let confidence = 0
if (name_col >= 0) {
const matched = [
org_number_col, email_col, phone_col, address_line1_col,
postal_code_col, city_col, vat_number_col, payment_terms_col,
].filter((c) => c !== null).length
confidence = 0.55 + Math.min(matched, 6) * 0.075
}
return {
name_col: name_col >= 0 ? name_col : 0,
org_number_col,
customer_type_col,
email_col,
phone_col,
address_line1_col,
address_line2_col,
postal_code_col,
city_col,
country_col,
vat_number_col,
payment_terms_col,
notes_col,
confidence: Math.min(Math.round(confidence * 100) / 100, 1),
}
}