LeadScraper als Frappe Custom App statt Standalone-Service #1

Closed
opened 2026-07-25 13:50:38 +00:00 by forgejo-admin · 4 comments

Kontext

Architektur-Entscheidung: LeadScraper wird nicht als eigenständiger Python-Service per REST-API an Frappe CRM angebunden, sondern als echte Frappe Custom App, die im selben Bench-Container wie CRM/ERPNext läuft und Frappes eigenen Scheduler/Queue-Worker nutzt statt eines externen REST-Clients.

Abhängigkeit: Erfordert eine laufende Frappe-Instanz. Diese wird im neuen Repo Docker-Stacks/frappe-crm-erp aufgebaut (siehe dortiges Issue "Docker-Stack: Frappe CRM + ERPNext (branch main) aufsetzen"). Umsetzung dieses Issues erst nach erfolgreichem Abschluss des Docker-Stacks.

Die bisherige Architektur-Doku in Projekt.md dieses Repos (Mautic-Nachfolger-Planung mit REST-API-Import) bleibt als Referenz für die fachliche Logik gültig (Website-/Impressum-Scan, Entscheidungsbaum, Tag-Struktur) — nur der technische Umsetzungsweg ändert sich von "externer Service + REST" zu "native Frappe App".

Grundprinzip

Die App läuft im selben Bench-Container wie CRM/ERPNext (installiert per bench get-app aus diesem Repo, dann bench install-app leadscraper). Kein eigener Container, keine REST-API-Anbindung — direkter Zugriff auf Frappes ORM.

Geplante Struktur

leadscraper/
├── hooks.py                      # App-Metadaten, scheduler_events, commands-Registrierung
├── modules.txt
├── leadscraper/
│   ├── google_places.py          # 1:1 Portierung aus search_leads.py (Google-Places-Suche)
│   ├── website_scan.py           # 1:1 Portierung aus import_leads.py (Website-/Impressum-Scan)
│   ├── region_resolver.py        # Unverändert aus altem Mautic-Projekt
│   ├── regions/                  # Unverändert: DE/AT/CH Region-JSONs (statische Referenzdaten)
│   ├── tasks.py                  # Orchestrierung: sucht + importiert, per frappe.enqueue
│   ├── commands.py               # `bench leadscraper search/import/run` (CLI wie bisher)
│   └── doctype/
│       └── lead_scraper_settings/ # Single-DocType: Google-API-Key (Password-Feld), Sprache,
│                                   # max_results_per_city, delay_between_requests
└── requirements.txt              # requests (kein Mautic-Client mehr nötig)

Kernentscheidungen (aus bisheriger Projekt.md, fachlich unverändert gültig)

  • Ziel-DocType: CRM Lead (aus der crm-App), nicht das ERPNext-CRM-Modul (laut Frappe zur Deprecation vorgesehen).
    → Exaktes Feld-Schema von CRM Lead muss nach Inbetriebnahme des Docker-Stacks geprüft werden (bench --site crm.local consolefrappe.get_meta("CRM Lead").fields), um das bestehende Feld-Mapping aus Projekt.md zu verifizieren/anzupassen.
  • Duplikatsprüfung per E-Mail ODER Firmenname → frappe.db.exists("CRM Lead", {...}).
  • Bei Duplikat: nur Kommentar/Notiz anhängen ("Erneut gefunden am TT.MM.JJJJ"), keine Feld-Aktualisierung.
  • Tags: Frappes Standard-Tag-System (frappe.desk.doctype.tag.tag.add_tag), automatisch angelegt falls nicht vorhanden — getrennte Tags für Suchbegriff und Region-Code.
  • Google-API-Key & Such-Defaults: kein config.json mehr, stattdessen Single-DocType Lead Scraper Settings (Secrets über Frappes verschlüsseltes Password-Feld statt Klartext-Datei).
  • Ausführung: bench leadscraper run --query "Polsterei" --regions BY (CLI, wie gewohnt) oder später ein Button in der CRM-UI, der frappe.enqueue(..., queue="long") auslöst und im queue-long-Worker des Docker-Stacks läuft.
  • Scheduler (hooks.py: scheduler_events) optional für wiederkehrende automatische Läufe — wird erst konkretisiert, wenn ein fester Rhythmus gewünscht ist (aktuell nicht gefordert, bleibt manuell/on-demand).

Offene Punkte für die Umsetzungsphase

  • Exaktes CRM Lead-Feldschema verifizieren (s.o.)
  • Klären, ob Notizen als Frappe-Comment (Timeline) oder als CRM-eigener Notes-DocType angelegt werden (abhängig von der crm-App-Struktur, nach Installation prüfbar)
  • requirements.txt der App vs. Bench-globale Python-Umgebung (Frappe-Apps installieren ihre Requirements in die gemeinsame Bench-venv — keine Konflikte mit requests erwartet)

Referenz

Wiederverwendbare Kernlogik aus dem alten Mautic-Projekt (GitLab/wsc_py_leadscraper): search_leads.py, import_leads.py, region_resolver.py, generate_regions.py — Mautic-unabhängig, direkt portierbar.

## Kontext Architektur-Entscheidung: LeadScraper wird **nicht** als eigenständiger Python-Service per REST-API an Frappe CRM angebunden, sondern als **echte Frappe Custom App**, die im selben Bench-Container wie CRM/ERPNext läuft und Frappes eigenen Scheduler/Queue-Worker nutzt statt eines externen REST-Clients. Abhängigkeit: Erfordert eine laufende Frappe-Instanz. Diese wird im neuen Repo `Docker-Stacks/frappe-crm-erp` aufgebaut (siehe dortiges Issue "Docker-Stack: Frappe CRM + ERPNext (branch main) aufsetzen"). **Umsetzung dieses Issues erst nach erfolgreichem Abschluss des Docker-Stacks.** Die bisherige Architektur-Doku in `Projekt.md` dieses Repos (Mautic-Nachfolger-Planung mit REST-API-Import) bleibt als Referenz für die fachliche Logik gültig (Website-/Impressum-Scan, Entscheidungsbaum, Tag-Struktur) — nur der technische Umsetzungsweg ändert sich von "externer Service + REST" zu "native Frappe App". ## Grundprinzip Die App läuft im selben Bench-Container wie CRM/ERPNext (installiert per `bench get-app` aus diesem Repo, dann `bench install-app leadscraper`). Kein eigener Container, keine REST-API-Anbindung — direkter Zugriff auf Frappes ORM. ## Geplante Struktur ``` leadscraper/ ├── hooks.py # App-Metadaten, scheduler_events, commands-Registrierung ├── modules.txt ├── leadscraper/ │ ├── google_places.py # 1:1 Portierung aus search_leads.py (Google-Places-Suche) │ ├── website_scan.py # 1:1 Portierung aus import_leads.py (Website-/Impressum-Scan) │ ├── region_resolver.py # Unverändert aus altem Mautic-Projekt │ ├── regions/ # Unverändert: DE/AT/CH Region-JSONs (statische Referenzdaten) │ ├── tasks.py # Orchestrierung: sucht + importiert, per frappe.enqueue │ ├── commands.py # `bench leadscraper search/import/run` (CLI wie bisher) │ └── doctype/ │ └── lead_scraper_settings/ # Single-DocType: Google-API-Key (Password-Feld), Sprache, │ # max_results_per_city, delay_between_requests └── requirements.txt # requests (kein Mautic-Client mehr nötig) ``` ## Kernentscheidungen (aus bisheriger `Projekt.md`, fachlich unverändert gültig) - Ziel-DocType: **CRM Lead** (aus der `crm`-App), nicht das ERPNext-CRM-Modul (laut Frappe zur Deprecation vorgesehen). → Exaktes Feld-Schema von `CRM Lead` muss nach Inbetriebnahme des Docker-Stacks geprüft werden (`bench --site crm.local console` → `frappe.get_meta("CRM Lead").fields`), um das bestehende Feld-Mapping aus `Projekt.md` zu verifizieren/anzupassen. - Duplikatsprüfung per E-Mail ODER Firmenname → `frappe.db.exists("CRM Lead", {...})`. - Bei Duplikat: nur Kommentar/Notiz anhängen ("Erneut gefunden am TT.MM.JJJJ"), keine Feld-Aktualisierung. - Tags: Frappes Standard-Tag-System (`frappe.desk.doctype.tag.tag.add_tag`), automatisch angelegt falls nicht vorhanden — getrennte Tags für Suchbegriff und Region-Code. - Google-API-Key & Such-Defaults: **kein `config.json` mehr**, stattdessen Single-DocType `Lead Scraper Settings` (Secrets über Frappes verschlüsseltes `Password`-Feld statt Klartext-Datei). - Ausführung: `bench leadscraper run --query "Polsterei" --regions BY` (CLI, wie gewohnt) oder später ein Button in der CRM-UI, der `frappe.enqueue(..., queue="long")` auslöst und im `queue-long`-Worker des Docker-Stacks läuft. - Scheduler (`hooks.py: scheduler_events`) optional für wiederkehrende automatische Läufe — wird erst konkretisiert, wenn ein fester Rhythmus gewünscht ist (aktuell nicht gefordert, bleibt manuell/on-demand). ## Offene Punkte für die Umsetzungsphase - [ ] Exaktes `CRM Lead`-Feldschema verifizieren (s.o.) - [ ] Klären, ob Notizen als Frappe-`Comment` (Timeline) oder als CRM-eigener Notes-DocType angelegt werden (abhängig von der `crm`-App-Struktur, nach Installation prüfbar) - [ ] `requirements.txt` der App vs. Bench-globale Python-Umgebung (Frappe-Apps installieren ihre Requirements in die gemeinsame Bench-venv — keine Konflikte mit `requests` erwartet) ## Referenz Wiederverwendbare Kernlogik aus dem alten Mautic-Projekt (`GitLab/wsc_py_leadscraper`): `search_leads.py`, `import_leads.py`, `region_resolver.py`, `generate_regions.py` — Mautic-unabhängig, direkt portierbar.
Author
Owner

Detailplanung abgeschlossen, gegen das laufende crm.local verifiziert (Teil 1 ist fertig: Docker-Stacks/frappe-crm-erp, Branch develop, Login funktioniert).

Das tatsächliche CRM Lead-Schema wurde direkt aus apps/crm/crm/fcrm/doctype/crm_lead/crm_lead.json im laufenden Container gelesen (66 Felder) — einige Annahmen aus der ursprünglichen Projekt.md treffen so nicht zu:

Korrigiertes Feld-Mapping

Datenpunkt CRM Lead Feld
Firma organization
Telefon mobile_no
Website website
E-Mail email
Inhaber-Name lead_name
Quelle source (Link auf CRM Lead Source, kein Freitext; Eintrag „Google Business" muss automatisch angelegt werden — existiert noch nicht)
Adresse (Straße/PLZ/Ort/Land) kein Feld auf CRM Lead vorhanden → eigene Custom Fields custom_address/custom_city/custom_country
google_maps_url / Bewertung Custom Fields custom_google_maps_url / custom_google_rating (wie geplant)
Tags kein Feld auf CRM Lead — läuft über Frappes generisches Tag-System (frappe.desk.doctype.tag.tag.add_tag, verifiziert: erstellt den Tag-Master automatisch)
Notiz bei Duplikat eigene FCRM Note-DocType gefunden (reference_doctype/reference_docname, Dynamic Link) — genau der Mechanismus für "Erneut gefunden am TT.MM.JJJJ"

Wichtiger neuer Befund: Persistenz

Das custom-Image aus Teil 1 deklariert nur sites/ und logs/ als Docker-Volumes (apps/ liegt im Image-Layer). Ein bench get-app live im laufenden Container wäre nach einer Container-Neuerstellung weg. Die App muss über apps.json in frappe-crm-erp eingetragen und das Image neu gebaut werden.

App-Struktur (final)

LeadScraper-Google/
├── hooks.py / pyproject.toml / modules.txt   # von `bench new-app` generiert
├── leadscraper/
│   ├── google_places.py            # portiert aus search_leads.py
│   ├── website_scan.py             # portiert aus import_leads.py (ohne MauticClient)
│   ├── region_resolver.py          # 1:1 aus altem Projekt
│   ├── regions/                    # 1:1 aus altem Projekt
│   ├── crm_lead_sync.py            # NEU: Duplikatsprüfung, FCRM-Note, Source-Autocreate, Tags
│   ├── tasks.py                    # Orchestrierung für frappe.enqueue
│   ├── commands.py                 # bench leadscraper search/import/run
│   └── doctype/lead_scraper_settings/  # Single-DocType, API-Key als Password-Feld
└── fixtures/custom_field.json      # Custom Fields auf CRM Lead, reproduzierbar

Umsetzung erfolgt jetzt Schritt für Schritt (wie bei frappe-crm-erp), beginnend mit bench new-app leadscraper im laufenden Container.

**Detailplanung abgeschlossen, gegen das laufende `crm.local` verifiziert (Teil 1 ist fertig: `Docker-Stacks/frappe-crm-erp`, Branch `develop`, Login funktioniert).** Das tatsächliche `CRM Lead`-Schema wurde direkt aus `apps/crm/crm/fcrm/doctype/crm_lead/crm_lead.json` im laufenden Container gelesen (66 Felder) — einige Annahmen aus der ursprünglichen `Projekt.md` treffen so nicht zu: ## Korrigiertes Feld-Mapping | Datenpunkt | CRM Lead Feld | |---|---| | Firma | `organization` | | Telefon | `mobile_no` | | Website | `website` | | E-Mail | `email` | | Inhaber-Name | `lead_name` | | Quelle | `source` (**Link** auf `CRM Lead Source`, kein Freitext; Eintrag „Google Business" muss automatisch angelegt werden — existiert noch nicht) | | Adresse (Straße/PLZ/Ort/Land) | **kein Feld auf CRM Lead vorhanden** → eigene Custom Fields `custom_address`/`custom_city`/`custom_country` | | google_maps_url / Bewertung | Custom Fields `custom_google_maps_url` / `custom_google_rating` (wie geplant) | | Tags | **kein Feld auf CRM Lead** — läuft über Frappes generisches Tag-System (`frappe.desk.doctype.tag.tag.add_tag`, verifiziert: erstellt den Tag-Master automatisch) | | Notiz bei Duplikat | eigene `FCRM Note`-DocType gefunden (`reference_doctype`/`reference_docname`, Dynamic Link) — genau der Mechanismus für "Erneut gefunden am TT.MM.JJJJ" | ## Wichtiger neuer Befund: Persistenz Das `custom`-Image aus Teil 1 deklariert nur `sites/` und `logs/` als Docker-Volumes (`apps/` liegt im Image-Layer). Ein `bench get-app` **live im laufenden Container** wäre nach einer Container-Neuerstellung weg. Die App muss über `apps.json` in `frappe-crm-erp` eingetragen und das Image neu gebaut werden. ## App-Struktur (final) ``` LeadScraper-Google/ ├── hooks.py / pyproject.toml / modules.txt # von `bench new-app` generiert ├── leadscraper/ │ ├── google_places.py # portiert aus search_leads.py │ ├── website_scan.py # portiert aus import_leads.py (ohne MauticClient) │ ├── region_resolver.py # 1:1 aus altem Projekt │ ├── regions/ # 1:1 aus altem Projekt │ ├── crm_lead_sync.py # NEU: Duplikatsprüfung, FCRM-Note, Source-Autocreate, Tags │ ├── tasks.py # Orchestrierung für frappe.enqueue │ ├── commands.py # bench leadscraper search/import/run │ └── doctype/lead_scraper_settings/ # Single-DocType, API-Key als Password-Feld └── fixtures/custom_field.json # Custom Fields auf CRM Lead, reproduzierbar ``` Umsetzung erfolgt jetzt Schritt für Schritt (wie bei `frappe-crm-erp`), beginnend mit `bench new-app leadscraper` im laufenden Container.
Author
Owner

Nachtrag zum Feld-Mapping: first_name ist auf CRM Lead pflicht (reqd: 1) — stand im ursprünglichen Feld-Dump, wurde aber übersehen, da der erste Dump das reqd-Flag nicht mit ausgegeben hat. Beim ersten Testlauf von crm_lead_sync.py flog prompt ein MandatoryError.

Lösung: Wird aus dem Impressum-Inhaber (falls gefunden, per Vor-/Nachname gesplittet) befüllt, sonst Fallback auf den Firmennamen — damit blockiert eine fehlende Inhaber-Angabe (der Normalfall bei dieser Datenquelle) nie die Lead-Anlage.

crm_lead_sync.py ist fertig und gegen den laufenden Stack end-to-end verifiziert (Neuanlage, Duplikat-Erkennung inkl. FCRM-Note, tote Website, keine Website/E-Mail — alle vier Pfade korrekt, Tags/Source/Custom-Fields stimmen).

**Nachtrag zum Feld-Mapping:** `first_name` ist auf `CRM Lead` pflicht (`reqd: 1`) — stand im ursprünglichen Feld-Dump, wurde aber übersehen, da der erste Dump das `reqd`-Flag nicht mit ausgegeben hat. Beim ersten Testlauf von `crm_lead_sync.py` flog prompt ein `MandatoryError`. Lösung: Wird aus dem Impressum-Inhaber (falls gefunden, per Vor-/Nachname gesplittet) befüllt, sonst Fallback auf den Firmennamen — damit blockiert eine fehlende Inhaber-Angabe (der Normalfall bei dieser Datenquelle) nie die Lead-Anlage. `crm_lead_sync.py` ist fertig und gegen den laufenden Stack end-to-end verifiziert (Neuanlage, Duplikat-Erkennung inkl. FCRM-Note, tote Website, keine Website/E-Mail — alle vier Pfade korrekt, Tags/Source/Custom-Fields stimmen).
Author
Owner

Korrektur zur CLI-Befehlsbenennung: Ursprünglich im Plan als bench leadscraper search/import/run vorgesehen — das gibt es so nicht. bench gruppiert Commands aus Apps nicht pro App (frappe/utils/bench_helper.py mischt alle Commands aller Apps in einen einzigen flachen Namespace). Um Kollisionen mit anderen Apps zu vermeiden, heißen die Befehle jetzt App-präfixiert:

  • bench leadscraper-search --query "..." --regions BY [--dry-run] [--output leads.json]
  • bench leadscraper-import --input leads.json --query "..."
  • bench leadscraper-run --query "..." --regions BY (beides in einem Rutsch)

tasks.py + commands.py sind fertig und gegen den laufenden Stack end-to-end verifiziert: --help-Registrierung, Dry-Run-Planung, echte Lead-Anlage per CLI inkl. Duplikaterkennung im zweiten Lauf.

Womit die fachliche App-Logik (Schritte 1–6 aus dem Plan) vollständig steht. Fehlt noch Schritt 7: die App dauerhaft machen (apps.json in frappe-crm-erp ergänzen, Image neu bauen, install-app auf dem gebauten Image statt nur live im laufenden Container).

**Korrektur zur CLI-Befehlsbenennung:** Ursprünglich im Plan als `bench leadscraper search/import/run` vorgesehen — das gibt es so nicht. `bench` gruppiert Commands aus Apps nicht pro App (`frappe/utils/bench_helper.py` mischt alle Commands aller Apps in einen einzigen flachen Namespace). Um Kollisionen mit anderen Apps zu vermeiden, heißen die Befehle jetzt App-präfixiert: - `bench leadscraper-search --query "..." --regions BY [--dry-run] [--output leads.json]` - `bench leadscraper-import --input leads.json --query "..."` - `bench leadscraper-run --query "..." --regions BY` (beides in einem Rutsch) `tasks.py` + `commands.py` sind fertig und gegen den laufenden Stack end-to-end verifiziert: `--help`-Registrierung, Dry-Run-Planung, echte Lead-Anlage per CLI inkl. Duplikaterkennung im zweiten Lauf. Womit die fachliche App-Logik (Schritte 1–6 aus dem Plan) vollständig steht. Fehlt noch Schritt 7: die App dauerhaft machen (`apps.json` in `frappe-crm-erp` ergänzen, Image neu bauen, `install-app` auf dem gebauten Image statt nur live im laufenden Container).
Author
Owner

Schritt 7 (App dauerhaft machen) abgeschlossen.

apps.json in frappe-crm-erp um LeadScraper-Google (Branch main) ergänzt, Image neu gebaut. Dabei ein reales Problem gefunden und behoben: BuildKit-Secrets (apps.json) fließen nicht in den Layer-Cache-Key ein — der erste Rebuild-Versuch lief komplett CACHED durch und produzierte trotz Exit-Code 0 ein Image ohne leadscraper. Fix: build.sh erzwingt jetzt bei jedem Aufruf einen frischen CACHE_BUST-Build-Arg (in frappe-crm-erp committet).

Zusätzlich auf Wunsch ergänzt:

  • docker-compose.dev-apps.yml in frappe-crm-erp (optional, nicht automatisch geladen): bind-mountet den lokalen App-Checkout in die laufenden Container, damit Code-Änderungen ohne 20–40-Min.-Rebuild sichtbar sind. Live getestet (Host-Änderung sofort im Container sichtbar), danach sauber zurückgebaut.
  • Klargestellt, was wo persistiert wird: App-Code ausschließlich über apps.json+Image (bzw. den Dev-Mount lokal) — bewusst kein Docker-Volume für apps/, damit das Image der reproduzierbare Deploy-Artefakt bleibt. App-Daten (Leads, Settings-Werte, Custom-Field-Inhalte) liegen unabhängig davon immer im sites-Volume.
  • docs/install.md in diesem Repo: vollständige Installationsanleitung inkl. Tabelle "was liegt wo" (Code/Schema/Secrets/Daten), beide Installationswege, CLI-Referenz, Troubleshooting-Sammlung.

Verifiziert nach Rebuild: leadscraper läuft nativ aus dem Image (bench list-apps), CLI-Befehle registriert, Login funktioniert, Lead Scraper Settings weiterhin korrekt (Daten haben den Rebuild unbeschadet überstanden, da sie im sites-Volume liegen, nicht im Image).

Damit ist der komplette Plan aus diesem Issue umgesetzt.

**Schritt 7 (App dauerhaft machen) abgeschlossen.** `apps.json` in `frappe-crm-erp` um `LeadScraper-Google` (Branch `main`) ergänzt, Image neu gebaut. Dabei ein reales Problem gefunden und behoben: BuildKit-Secrets (`apps.json`) fließen nicht in den Layer-Cache-Key ein — der erste Rebuild-Versuch lief komplett `CACHED` durch und produzierte trotz Exit-Code 0 ein Image **ohne** `leadscraper`. Fix: `build.sh` erzwingt jetzt bei jedem Aufruf einen frischen `CACHE_BUST`-Build-Arg (in `frappe-crm-erp` committet). Zusätzlich auf Wunsch ergänzt: - `docker-compose.dev-apps.yml` in `frappe-crm-erp` (optional, nicht automatisch geladen): bind-mountet den lokalen App-Checkout in die laufenden Container, damit Code-Änderungen ohne 20–40-Min.-Rebuild sichtbar sind. Live getestet (Host-Änderung sofort im Container sichtbar), danach sauber zurückgebaut. - Klargestellt, was wo persistiert wird: App-**Code** ausschließlich über `apps.json`+Image (bzw. den Dev-Mount lokal) — bewusst **kein** Docker-Volume für `apps/`, damit das Image der reproduzierbare Deploy-Artefakt bleibt. App-**Daten** (Leads, Settings-Werte, Custom-Field-Inhalte) liegen unabhängig davon immer im `sites`-Volume. - `docs/install.md` in diesem Repo: vollständige Installationsanleitung inkl. Tabelle "was liegt wo" (Code/Schema/Secrets/Daten), beide Installationswege, CLI-Referenz, Troubleshooting-Sammlung. Verifiziert nach Rebuild: `leadscraper` läuft nativ aus dem Image (`bench list-apps`), CLI-Befehle registriert, Login funktioniert, `Lead Scraper Settings` weiterhin korrekt (Daten haben den Rebuild unbeschadet überstanden, da sie im `sites`-Volume liegen, nicht im Image). Damit ist der komplette Plan aus diesem Issue umgesetzt.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Frappe-Projekte/LeadScraper-Google#1
No description provided.