Files
crm/importer.py
T
MachielandClaude Opus 5.5 bffa053c05 v0.0.1: eerste versie van de Van Emous CRM
Flask + SQLite, in de Van Emous-huisstijl (tokens uit het design system, GSAP-effecten
volgens de motion-regels, uit bij prefers-reduced-motion).

- Leads en contactpersonen: status, toewijzen, verwachte waarde, omzetten naar klant,
  tijdlijn, bestanden, taken/opvolging
- Relaties (zoals accounts in EspoCRM): klantbundels (huishouden/familie) en partners met
  type (leverancier, aannemer, ...); leveranciers van producten worden automatisch partners
- API voor formulieren (POST /api/leads) met dubbele-leaddetectie, bestanden en
  configurator-ontwerpen (house-design JSON)
- Import van CSV/JSON (leads, relaties, producten) met kolomkoppeling; configurator-formaat
- Producten met SKU als vaste sleutel, mutatiehistorie, POST /api/products, CSV-export;
  offerteregels houden hun eigen prijs
- Offertes met btw per tarief en afdrukversie (PDF via de browser)
- Inbox via IMAP/SMTP, gekoppeld aan contacten
- Gebruikers (beheerder/medewerker) en klantportaal (offertes accepteren, bestanden, berichten)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:56:55 +02:00

131 lines
6.1 KiB
Python

"""CSV/JSON inlezen voor de import, en het herkennen van kolomnamen.
CSV: scheidingsteken `;`, `,` of tab (automatisch), UTF-8 met of zonder BOM (of Windows-1252),
regels die met `#` beginnen worden overgeslagen — zoals de CSV's van de configurator.
JSON: een lijst met objecten, een object met daarin een lijst (bijv. {"contacten": [...]}) of één object.
"""
import csv
import json
FIELD_ALIASES = {
'name': ['name', 'naam', 'full_name', 'fullname', 'volledige_naam', 'your-name', 'klant', 'klantnaam'],
'first_name': ['first_name', 'firstname', 'voornaam'],
'last_name': ['last_name', 'lastname', 'achternaam'],
'email': ['email', 'e-mail', 'e_mail', 'emailadres', 'e-mailadres', 'mail', 'your-email'],
'phone': ['phone', 'telefoon', 'tel', 'telefoonnummer', 'phone_number', 'mobiel', 'your-phone'],
'company': ['company', 'bedrijf', 'bedrijfsnaam', 'organisatie'],
'relation': ['relatie', 'relation', 'account', 'huishouden', 'familie', 'account_name'],
'relation_role': ['rol', 'role', 'relatie_rol', 'functie'],
'address': ['address', 'adres', 'straat', 'street'],
'postcode': ['postcode', 'zip', 'zipcode', 'postal_code'],
'city': ['city', 'plaats', 'woonplaats', 'stad'],
'message': ['message', 'bericht', 'opmerking', 'opmerkingen', 'vraag', 'comments', 'your-message'],
'source': ['source', 'bron', 'formulier', 'form', 'form_name', 'lead_source'],
'status': ['status', 'fase'],
'value': ['value', 'waarde', 'bedrag', 'budget', 'verwachte_waarde'],
'notes': ['notes', 'notities', 'notitie'],
}
PRODUCT_ALIASES = {
'name': ['name', 'naam', 'product', 'productnaam', 'titel', 'omschrijving'],
'sku': ['sku', 'code', 'artikelnummer', 'artikelnr', 'artikel'],
'description': ['description', 'beschrijving', 'toelichting', 'details'],
'unit': ['unit', 'eenheid'],
'price': ['price', 'prijs', 'waarde', 'verkoopprijs', 'prijs_excl'],
'vat': ['vat', 'btw', 'btw%', 'btw_percentage'],
'supplier': ['supplier', 'leverancier'],
'active': ['active', 'actief'],
}
RELATION_ALIASES = {
'name': ['name', 'naam', 'bedrijf', 'bedrijfsnaam', 'relatie', 'organisatie', 'leverancier', 'familie'],
'kind': ['kind', 'soort'],
'type': ['type', 'categorie', 'branche'],
'email': ['email', 'e-mail', 'emailadres', 'e-mailadres', 'mail'],
'phone': ['phone', 'telefoon', 'tel', 'telefoonnummer'],
'website': ['website', 'url', 'site', 'web'],
'address': ['address', 'adres', 'straat'],
'postcode': ['postcode', 'zip', 'zipcode'],
'city': ['city', 'plaats', 'woonplaats', 'vestigingsplaats'],
'kvk': ['kvk', 'kvk-nummer', 'kvk_nummer', 'kvknummer', 'coc'],
'notes': ['notes', 'notities', 'opmerking', 'opmerkingen'],
}
TARGETS = {
'contacts': [('', '— overslaan —'), ('name', 'Naam'), ('first_name', 'Voornaam'), ('last_name', 'Achternaam'),
('email', 'E-mail'), ('phone', 'Telefoon'), ('company', 'Bedrijf'), ('address', 'Adres'),
('postcode', 'Postcode'), ('city', 'Plaats'), ('source', 'Bron'), ('status', 'Status'),
('value', 'Verwachte waarde'), ('notes', 'Notities'), ('message', 'Bericht (in tijdlijn)'),
('relation', 'Relatie (naam)'), ('relation_role', 'Rol binnen relatie'),
('extra', 'Extra veld (bewaren)')],
'relations': [('', '— overslaan —'), ('name', 'Naam'), ('kind', 'Soort (klant/partner)'), ('type', 'Type'),
('email', 'E-mail'), ('phone', 'Telefoon'), ('website', 'Website'), ('address', 'Adres'),
('postcode', 'Postcode'), ('city', 'Plaats'), ('kvk', 'KvK-nummer'), ('notes', 'Notities')],
'products': [('', '— overslaan —'), ('name', 'Naam'), ('sku', 'Artikelnummer / code'),
('description', 'Omschrijving'), ('unit', 'Eenheid'), ('price', 'Prijs excl. btw'),
('vat', 'Btw %'), ('supplier', 'Leverancier'), ('active', 'Actief (ja/nee)')],
}
class ImportFout(ValueError):
pass
def _key(name):
return str(name).strip().lower().replace(' ', '_')
def lookup(aliases):
return {alias: field for field, names in aliases.items() for alias in names}
def guess(cols, kind):
"""Stel per kolom een doelveld voor; elk doelveld maar één keer (de eerste kolom wint)."""
table = lookup({'contacts': FIELD_ALIASES, 'products': PRODUCT_ALIASES, 'relations': RELATION_ALIASES}[kind])
used, mapping = set(), []
for col in cols:
target = table.get(_key(col), '')
if target in used:
target = ''
if target:
used.add(target)
mapping.append(target or ('extra' if kind == 'contacts' else ''))
return mapping
def read(path):
"""Geef (kolommen, rijen) terug; rijen zijn dicts."""
with open(path, 'rb') as f:
raw = f.read()
try:
text = raw.decode('utf-8-sig')
except UnicodeDecodeError:
text = raw.decode('cp1252')
if path.endswith('.json'):
try:
data = json.loads(text)
except json.JSONDecodeError as e:
raise ImportFout(f'Geen geldige JSON: {e}') from e
if isinstance(data, dict):
if data.get('type') == 'house-design':
raise ImportFout('Dit is een ontwerp uit de configurator. Upload het bij een contact onder Bestanden.')
lists = [v for v in data.values() if isinstance(v, list)]
data = lists[0] if lists else [data]
if not isinstance(data, list):
raise ImportFout('Verwacht een lijst met objecten.')
rows = [r for r in data if isinstance(r, dict)]
cols = []
for row in rows:
cols += [k for k in row if k not in cols]
return cols, rows
lines = [line for line in text.splitlines() if line.strip() and not line.lstrip().startswith('#')]
if not lines:
raise ImportFout('Het bestand is leeg.')
delimiter = max([';', ',', '\t'], key=lines[0].count)
reader = csv.reader(lines, delimiter=delimiter)
cols = [c.strip() for c in next(reader)]
rows = [dict(zip(cols, (v.strip() for v in r))) for r in reader if any(v.strip() for v in r)]
return cols, rows