210 lines
14 KiB
Markdown
210 lines
14 KiB
Markdown
# Van Emous CRM
|
||
|
||
Eenvoudige CRM in Python (Flask) met SQLite, in de Van Emous-huisstijl. Versie 0.0.1.
|
||
|
||
## Functies
|
||
|
||
- **Leads en klanten** (zoals in EspoCRM): status (nieuw → in behandeling → offerte → gewonnen/verloren),
|
||
toewijzen aan een gebruiker, verwachte waarde, bron, omzetten van lead naar klant, tijdlijn met
|
||
notities, e-mail, statuswissels en formulierinzendingen, bestanden en afbeeldingen, CSV-export.
|
||
- **Relaties** (zoals accounts in EspoCRM), in twee soorten: **klanten** (bundels zoals een huishouden,
|
||
familie of bedrijf, met meerdere contactpersonen en hun rol) en **partners** met een type (leverancier,
|
||
aannemer, installateur, ...; de typen pas je aan bij Instellingen). Een lead omzetten naar klant zet de
|
||
persoon in een nieuwe of bestaande klantrelatie. Leveranciers van producten worden automatisch partners.
|
||
- **Gekozen model** per lead/contact: een woningontwerp uit de configurator (`house-design`-JSON, via het
|
||
formulier, de API of geüpload bij Bestanden) wordt het gekozen model; het nieuwste telt, een ander ontwerp
|
||
kies je op de contactpagina. Daar staat het in een **3D-viewer** op de engine van de configurator
|
||
(`/files/<id>/viewer`): 3D, doorsnede en plattegrond, met maten, m², inhoud, gewicht en een indicatieve
|
||
prijs (uit de kosten-CSV's van de configurator). In de leadlijst staat het model als kolom.
|
||
Met **Lagen** in de viewer kies je een bouwfase (t/m regelwerk, elektra, beplating, …) of losse lagen
|
||
(regelwerk, elektra, kozijnen, dak, …), zoals Lagen in de configurator, zodat de ploeg de opbouw kan bekijken.
|
||
- **PDF's per ontwerp** (`HousePdf` van de configurator, gemaakt in de viewer en bewaard bij het contact): een
|
||
**werktekening** (alle tekenbladen, elektra, zaaglijst en kosten; intern, niet in het klantportaal) en een
|
||
**klant-PDF** zoals de light-configurator die maakt (`LightPdf`: voorblad met beeld, kerncijfers, keuzes, indicatieve
|
||
prijs en QR-code, dan aanzichten en plattegrond en de werkwijze; wel in het portaal). Ontbreken ze, dan maakt de viewer ze vanzelf bij
|
||
het eerste openen; opnieuw maken via PDF in de weergave. Op de contactpagina bij het gekozen model en op de
|
||
relatiepagina (Ontwerpen en tekeningen, voor alle contactpersonen).
|
||
- Op mobiel staan bij contact en relatie de gegevens bovenaan, de 3D-weergave verderop.
|
||
- **Taken/opvolging** per contact met datum en eigenaar; "Mijn taken" op het dashboard.
|
||
- **API voor formulieren** (`POST /api/leads`): JSON, gewone formulieren en multipart met bestanden.
|
||
Nederlandse en Engelse veldnamen worden herkend, onbekende velden bewaard. Dubbele leads (zelfde
|
||
e-mail) komen bij het bestaande contact. Er is een honeypot (`_gotcha`) en een `redirect` naar een
|
||
bedankpagina. Een `house-design`-object uit de configurator wordt als ontwerp bij de lead opgeslagen.
|
||
Voorbeelden staan onder Instellingen.
|
||
- **Import** van CSV en JSON voor leads/contactpersonen, relaties en producten, met een voorbeeld en kolomkoppeling.
|
||
Het formaat van de configurator (`;`, komma-decimalen, BOM, `#`-regels) werkt direct.
|
||
- **Mailboxen** (Instellingen › Mailboxen): meerdere accounts naast elkaar (info@, facturen@, nieuwsbrief@,
|
||
persoonlijk), elk met eigen IMAP/SMTP, afzender en rol (inbox, facturen, nieuwsbrief). Toegang per gebruiker;
|
||
zonder gekozen gebruikers is een mailbox gedeeld met iedereen. Voorinstellingen voor OVHcloud (Zimbra / MX Plan)
|
||
en Gmail, beveiliging per server, een kopie van verstuurde mail in een IMAP-map en een verbindingstest.
|
||
- **Inbox**: e-mail uit de eigen mailboxen (per mailbox of samen), automatisch gekoppeld aan contacten (op
|
||
e-mailadres), contact aanmaken vanuit een mail, beantwoorden vanuit dezelfde mailbox. Nieuwe e-mail opstellen
|
||
(ook doorsturen) met keuze van de mailbox, CC en bijlagen; mappen Verzonden en Archief (archiveren raakt de
|
||
mailbox op de server niet), zoeken, als ongelezen markeren en alles als gelezen markeren.
|
||
- **Meldingen**: bel in de bovenbalk (bijv. nieuwe e-mail in een mailbox, nieuwsbrief verzonden), met toestemming
|
||
ook als bureaubladmelding. Routines melden via `notify()`.
|
||
- **Routines** (beheer): terugkerende taken met een interval en eigen instellingen, met logboek en "Nu uitvoeren":
|
||
e-mail ophalen, nieuwsbrieven versturen, opschonen. Nieuwe soorten komen erbij met `@jobs.register` (zie
|
||
`jobs.py`). Ze draaien via `python app.py scheduler` (Docker: container `crm-routines`) of cron met
|
||
`python app.py tick`; een routine draait nooit twee keer tegelijk.
|
||
- **Nieuwsbrieven**: segmenten (filters op soort, status, bron, toegewezen, plaats, datum, ontwerp) en
|
||
nieuwsbrieven met velden per ontvanger, testmail, inplannen en versturen per batch over één verbinding (de
|
||
limiet van de mailserver blijft buiten bereik), pauzeren en stoppen. Elke mail heeft een afmeldlink en
|
||
`List-Unsubscribe` (één klik); afgemelde en onbestelbare adressen (afleverberichten in een mailbox) vallen buiten
|
||
elk segment, per adres gaat er één mail uit.
|
||
- **E-mailtemplates**: onderwerp en tekst met velden als `{voornaam}`, `{model}` en `{afzender}`, te kiezen bij een
|
||
nieuwe e-mail, een antwoord, op de pagina van een contact en bij een nieuwsbrief. Velden worden direct ingevuld als het contact bekend
|
||
is, anders bij het versturen (contact op het eerste adres in Aan).
|
||
- **Agenda**: een agenda per gebruiker, samen in een maandoverzicht met de taken die een datum hebben; afspraken
|
||
aan een contact koppelen (ook op de contactpagina en het dashboard), eenvoudige herhaling. Via **CalDAV**
|
||
(`/dav/`, ontdekking via `/.well-known/caldav`) zet elke gebruiker zijn agenda op iPhone, Mac, Android (DAVx⁵)
|
||
of in Thunderbird; inloggen met e-mailadres en een app-wachtwoord uit Mijn account.
|
||
- **Zoeken en Nieuw** in de bovenbalk: één zoekveld voor leads, contacten, relaties, offertes, afspraken,
|
||
producten en e-mail (live resultaten, `/` springt erheen), en een knop om direct iets nieuws aan te maken.
|
||
- **Producten** met SKU als vaste sleutel, prijs, btw, eenheid, leverancier (partner) en afbeelding.
|
||
Een import of `POST /api/products` werkt producten bij op SKU (alleen de meegegeven velden) en legt
|
||
elke mutatie vast met de oude en de nieuwe waarde. Optioneel worden producten die niet meer in de lijst
|
||
staan inactief gezet. Offerteregels bewaren hun eigen prijs, dus bestaande offertes veranderen niet. Een
|
||
product dat in offertes gebruikt is, wordt bij verwijderen gearchiveerd. Export naar CSV in hetzelfde
|
||
formaat als de import.
|
||
- **Offertes** met regels (product of vrije regel), btw per tarief en een afdrukversie die je als PDF
|
||
opslaat via de browser.
|
||
- **Bestek uit ontwerp** bij een offerte (of "Offerte met bestek" bij het gekozen model): de engine van de
|
||
configurator rekent het ontwerp door zoals Kosten & tijden (`static/bestek.js`, `CostModel.compute` met de
|
||
kosten-CSV's) en zet alle posten met hoeveelheden in hoofdstukken (casco, isolatie, gevel, dak, vloer,
|
||
kozijnen, afwerking, inrichting, verlichting, elektra met wcd's, schakelaars en meters leiding per type,
|
||
transport en plaatsing). Elke post krijgt zijn kostprijs plus algemene kosten en marge; samen precies het
|
||
totaal van de configurator. Schroeven, zaag- en boorwerk staan per hoofdstuk samen als "Bevestiging en
|
||
bewerking". In de offerte per post (standaard: aantal × prijs per eenheid) of per hoofdstuk; het
|
||
afrondingsverschil van de prijzen per eenheid gaat naar de laatste regel van 1 post zonder product, zodat
|
||
het totaal precies klopt. Het bestek zelf komt als bijlage (zonder prijzen) in de afdrukversie en het portaal.
|
||
- **Producten uit het bestek:** posten met een vaste prijs per eenheid (wcd, schakelaar, aansluitpunten,
|
||
leidingen per type, spots en lampen, toilet, douche, keuken per m, deuren, ramen per maat, gevel- en
|
||
dakbedekking per m², isolatie, plaatmateriaal, ...) worden gekoppeld aan een product met de kostencode van de
|
||
configurator als SKU (`elektra.wcdDubbel`, `inrichting.toilet`, `kozijnen.raam.1200x500`; dezelfde codes als
|
||
in `kosten/leveranciers.csv`). Ontbreekt het product, dan wordt het aangemaakt met de prijs per eenheid van de
|
||
berekening; bestaande productprijzen worden alleen bijgewerkt als je dat aanvinkt (met producthistorie).
|
||
De regels staan in `PRODUCT_RULES` in `static/bestek.js`.
|
||
- **Gebruikers** met de rollen beheerder (ook gebruikers en instellingen) en medewerker.
|
||
- **Klantportaal** (`/portal`): de klant krijgt een uitnodigingslink (kopiëren of mailen vanaf het
|
||
contact), stelt een wachtwoord in en kan dan offertes bekijken, accepteren of afwijzen, bestanden
|
||
uploaden en berichten sturen. Een geaccepteerde offerte zet het contact op gewonnen/klant.
|
||
- **Effecten** met GSAP (cdnjs, 3.12.5) volgens de motion-regels van de huisstijl, in `static/fx.js`.
|
||
Ze staan uit bij `prefers-reduced-motion`, en na 3 seconden is de inhoud altijd zichtbaar.
|
||
|
||
## Starten
|
||
|
||
```sh
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements.txt
|
||
.venv/bin/python app.py # http://127.0.0.1:5000
|
||
```
|
||
|
||
De viewer heeft de engine van de configurator nodig (niet in git, ca. 160 MB met texturen):
|
||
|
||
```sh
|
||
tools/configurator-sync.sh [../configurator] # kopieert js/, assets/, kosten/ naar static/configurator/
|
||
```
|
||
|
||
Opnieuw draaien als er in de configurator iets aan de weergave verandert; `deploy.sh` doet dit zelf bij elke deploy.
|
||
|
||
Routines en e-mail vanaf de opdrachtregel:
|
||
|
||
```sh
|
||
.venv/bin/python app.py scheduler # blijft draaien en voert de routines uit die aan de beurt zijn
|
||
.venv/bin/python app.py tick # één keer (voor cron elke minuut)
|
||
.venv/bin/python app.py sync # nu e-mail ophalen uit alle actieve mailboxen
|
||
```
|
||
|
||
Demoleads (lorem ipsum, elk met een standaardmodel als gekozen model):
|
||
|
||
```sh
|
||
node tools/export-models.js # standaardmodellen (HouseModel.MODELS) → demo/modellen/*.json
|
||
.venv/bin/python app.py demo # leads aanmaken; bestaande demoleads worden overgeslagen
|
||
```
|
||
|
||
Bij de eerste keer maak je het beheerdersaccount aan. Daarna vul je onder Instellingen de
|
||
bedrijfsgegevens, de e-mail en de toegestane websites voor de API in.
|
||
|
||
## Online zetten (server)
|
||
|
||
Op een nieuwe Ubuntu-server (26.04 LTS) eenmalig, nadat de DNS van het domein naar de server wijst:
|
||
|
||
```sh
|
||
scp -r server ubuntu@SERVER:
|
||
ssh ubuntu@SERVER sudo bash server/setup.sh crm.voorbeeld.nl jij@voorbeeld.nl
|
||
```
|
||
|
||
`server/setup.sh` installeert Python, nginx, certbot, rsync en sqlite3, zet de tijdzone op Europe/Amsterdam,
|
||
maakt de gebruiker `crm` met `/srv/vanemous-crm` (`releases/`, `current`, `data/`), de systemd-dienst
|
||
`vanemous-crm` (gunicorn op 127.0.0.1:5050, `CRM_PROXY=1`), de nginx-site met HTTPS, de firewall (SSH, 80, 443),
|
||
automatische beveiligingsupdates en cron (elke minuut de routines, elke nacht een databaseback-up, 7 dagen rond).
|
||
|
||
Een versie uitrollen (of terugrollen) vanaf je eigen machine:
|
||
|
||
```sh
|
||
tools/release.sh crm@crm.voorbeeld.nl # nieuwste tag
|
||
tools/release.sh crm@crm.voorbeeld.nl v0.0.1 # bepaalde versie
|
||
```
|
||
|
||
Dat stuurt `git archive <tag>` en de 3D-engine uit `../configurator` naar de server; `server/activate.sh` maakt daar
|
||
een venv, back-upt de database, zet `current` om en herstart de dienst. Start de nieuwe versie niet, dan gaat hij
|
||
terug naar de vorige. Logboek: `sudo journalctl -u vanemous-crm`.
|
||
|
||
`CRM_PROXY=1` laat de app de `X-Forwarded-Proto`/`-Host`-headers van de proxy volgen (links naar buiten, zoals
|
||
portaaluitnodigingen, worden dan `https://domein/...`) en zet sessiecookies op alleen-HTTPS. De proxy moet
|
||
uploads tot 25 MB doorlaten (nginx: `client_max_body_size 25m`).
|
||
|
||
Lokaal zoals op de server, maar zonder nginx: `./deploy.sh` (zie het script).
|
||
|
||
Back-ups buiten de server (bijv. de back-upoptie van OVH of een rsync van `/srv/vanemous-crm/data`) regel je apart:
|
||
de nachtelijke kopie staat op dezelfde schijf en dekt `uploads/` niet.
|
||
|
||
### Tectum (configurator) op dezelfde server
|
||
|
||
De configurator (`../configurator`) staat als statische site op een eigen subdomein, helemaal achter één
|
||
wachtwoord (nginx basic auth). Eenmalig, na `server/setup.sh`:
|
||
|
||
```sh
|
||
scp -r server ubuntu@SERVER:
|
||
ssh -t ubuntu@SERVER sudo bash server/tectum-setup.sh tectum.voorbeeld.nl jij@voorbeeld.nl [gebruikersnaam]
|
||
```
|
||
|
||
Dat vraagt het wachtwoord (gebruikersnaam standaard `vanemous`; wijzigen: `sudo WACHTWOORD=1 bash ...`), maakt
|
||
`/srv/www/tectum` (van `crm`, zodat releases via dezelfde SSH-toegang gaan) en de nginx-site met HTTPS.
|
||
Uitrollen of terugrollen vanaf je eigen machine, vanuit een tag van de configurator:
|
||
|
||
```sh
|
||
tools/tectum-release.sh crm@tectum.voorbeeld.nl # nieuwste tag
|
||
tools/tectum-release.sh crm@tectum.voorbeeld.nl v0.7.6 # bepaalde versie
|
||
```
|
||
|
||
## Opslag
|
||
|
||
Alles staat in `data/`: `crm.db` (SQLite), `uploads/` (bestanden), `secret.key` (sessies).
|
||
Een back-up is een kopie van die map. Nieuwe kolommen worden bij het starten automatisch aan een
|
||
bestaande database toegevoegd (`MIGRATIONS` in `app.py`). Houd `data/` buiten de webroot: er staan ook
|
||
mailwachtwoorden in.
|
||
|
||
`CRM_DATA=/ander/pad` verplaatst de datamap, `HOST` en `PORT` stellen de ontwikkelserver in.
|
||
|
||
## Structuur
|
||
|
||
```
|
||
app.py routes en logica
|
||
importer.py CSV/JSON inlezen en kolommen herkennen
|
||
mail.py IMAP/SMTP (voorinstellingen, verbindingstest, kopie in Verzonden, bulk via Outbox, bounces)
|
||
jobs.py routines: types registreren en hun instellingen (uitvoeren: run_job in app.py)
|
||
ical.py iCalendar lezen/schrijven en herhalingen uitvouwen (agenda en CalDAV)
|
||
schema.sql database
|
||
templates/ pagina's (Jinja)
|
||
static/ style.css (huisstijltokens), fx.js (GSAP-effecten), search.js (zoekveld), notify.js (bel), compose.js
|
||
(e-mail opstellen), logo.svg, viewer.js (3D-viewer), bestek.js
|
||
static/configurator/ engine van de configurator (tools/configurator-sync.sh, niet in git)
|
||
tools/ configurator-sync.sh, export-models.js (standaardmodellen als JSON)
|
||
demo/modellen standaardmodellen als house-design-JSON voor de demoleads
|
||
```
|
||
|
||
De plattegrond in `static/viewer.js` is overgenomen uit `js/light/light.js` (`drawPlan`) van de
|
||
configurator; verandert die, dan hier meenemen.
|