154 lines
9.7 KiB
Markdown
154 lines
9.7 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.
|
||
- **Inbox**: IMAP ophalen en SMTP versturen, mails automatisch gekoppeld aan contacten (op e-mailadres),
|
||
contact aanmaken vanuit een mail, beantwoorden.
|
||
- **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.
|
||
|
||
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 (e-mail ophalen elke 10 minuten, 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.
|
||
|
||
## 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
|
||
schema.sql database
|
||
templates/ pagina's (Jinja)
|
||
static/ style.css (huisstijltokens), fx.js (GSAP-effecten), 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.
|