81e9dd224e
* 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
107 lines
3.5 KiB
TypeScript
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),
|
|
}
|
|
}
|