Files

233 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/vervallen),
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.
- **Massa-acties en CSV-export** in de lijsten van contacten/leads, relaties, producten en offertes: rijen aanvinken
(shift-klik voor een reeks, of alles op de pagina) en in de balk onderin een actie kiezen. Contacten: status
wijzigen, toewijzen, omzetten naar klant, exporteren, verwijderen; relaties: type wijzigen, exporteren, verwijderen;
producten: actief/inactief, exporteren, verwijderen (in offertes gebruikt → inactief); offertes: status wijzigen
(met dezelfde gevolgen voor het contact als één voor één), exporteren, verwijderen. Statuswissels en toewijzingen
komen per contact in de tijdlijn. **Exporteer CSV** bovenaan neemt de filters en zoekterm van de lijst mee en levert
alle velden (`;`, BOM, voor Excel): bij contacten ook soort, statuslabel, toegewezen gebruiker, relatie, model,
portaaltoegang en elk extra formulierveld als eigen kolom (portaaltoken en wachtwoordhash nooit).
- **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.
- **Documenten met versies** bij leads/klanten en bij relaties: elk document heeft een of meer versies (nieuwe
versie uploaden met een opmerking, oude versies blijven te openen en te verwijderen). Soorten: bestand, woning
en de tekeningen (werktekening, klant-PDF) van een woning. Bestanden van vóór v0.0.7 worden bij het starten
vanzelf versie 1 van een eigen document.
- **Woning** per lead/klant: een woningontwerp uit Tectum (`house-design`-JSON, via het formulier, de API, geüpload
bij Documenten of bewaard vanuit Tectum) is een woning met versies. Een nieuwe woning wordt de gekozen woning
(de nieuwste telt), een andere kies je op de contactpagina. Daar staat de nieuwste versie in een **3D-viewer** op
de engine van de configurator (`/files/<id>/viewer`, een oudere versie via de keuzelijst): 3D, doorsnede en
plattegrond, met maten, m², inhoud, gewicht en een indicatieve prijs (uit de kosten-CSV's van de configurator).
Zonder woning is het blok er niet. In de leadlijst staat de woning als kolom (Model).
- **Tectum** (in de stack): **Bewerken in Tectum** bij de woning opent hem op `tectum.<domein>/?woning=<id>`; daar
bewaart **Opslaan in CRM** een nieuwe versie. Tectum toont de woningen van leads en klanten ook bij Nieuw. De API
daarvoor is `/api/tectum/woningen` (lijst, `?q=`), `/api/tectum/woningen/<id>` (ontwerp, `?versie=`) en `POST
/api/tectum/woningen/<id>/versies` (`{ontwerp, opmerking, basis}`; 409 als er intussen een nieuwere versie is),
alleen met de sleutel `TECTUM_CRM_KEY` (Caddy stuurt die namens Tectum mee; leeg = uit). `TECTUM_URL` = adres
van Tectum voor de knop.
- **Kaart** bij een lead/klant (onder de woning) en een klantrelatie zodra het adres bekend is (straat met
huisnummer en postcode of plaats, of anders het adres uit het woningontwerp): de kaveltoets van de kaart-app
(`KAART_URL`, standaard `/kaart/`, in de stack op hetzelfde domein). Kwam de lead uit de kaveltoets, dan opent de
kaart die kavel met de getekende woningen. Zonder adres is het blok er niet.
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). Per woningversie: ontbreken
ze voor de nieuwste versie, dan maakt de viewer ze vanzelf bij het openen; opnieuw maken via PDF in de weergave.
Op de contactpagina bij de woning en op de relatiepagina (Woningen 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; massa-actie actief/inactief maken of verwijderen (jezelf wordt overgeslagen). Verwijderen laat contacten, taken en activiteiten zonder eigenaar staan; agenda, meldingen en mailboxkoppelingen gaan mee.
- **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.