Docs: Aktualisierung der Dokumentation für Task [31588f42]

This commit is contained in:
2026-03-03 08:37:11 +00:00
parent ad3770fb00
commit e01fdb65dc
5 changed files with 3187 additions and 352 deletions

View File

@@ -283,6 +283,15 @@ When creating sales via API, specific constraints apply due to the shared tenant
* `Person`: Highly recommended linking to a specific person, not just the company.
* **Context:** Avoid creating sales on the parent company "Wackler Service Group" (ID 3). Always target the specific lead company.
### Analyse der SuperOffice `Sale`-Entität (März 2026)
- **Ziel:** Erstellung eines Reports, der abbildet, welche Kunden welche Produkte angeboten bekommen oder gekauft haben. Die initiale Vermutung war, dass Produktinformationen oft als Freitext-Einträge und nicht über den offiziellen Produktkatalog erfasst werden.
- **Problem:** Die Untersuchung der Datenstruktur zeigte, dass die API-Endpunkte zur Abfrage von `Quote`-Objekten (Angeboten) und `QuoteLines` (Angebotspositionen) über `Sale`-, `Contact`- oder `Project`-Beziehungen hinweg nicht zuverlässig funktionierten. Viele Abfragen resultierten in `500 Internal Server Errors` oder leeren Datenmengen, was eine direkte Verknüpfung von Verkauf zu Produkt unmöglich machte.
- **Kern-Erkenntnis (Datenstruktur):**
1. **Freitext statt strukturierter Daten:** Die Analyse eines konkreten `Sale`-Objekts (ID `342243`) bestätigte die ursprüngliche Hypothese. Produktinformationen (z.B. `2xOmnie CD-01 mit Nachlass`) werden direkt in das `Heading`-Feld (Betreff) des `Sale`-Objekts als Freitext eingetragen. Es existieren oft keine verknüpften `Quote`- oder `QuoteLine`-Entitäten.
2. **Datenqualität bei Verknüpfungen:** Eine signifikante Anzahl von `Sale`-Objekten im System weist keine Verknüpfung zu einem `Contact`-Objekt auf (`Contact: null`). Dies erschwert die automatische Zuordnung von Verkäufen zu Kunden erheblich.
- **Nächster Schritt / Lösungsweg:** Ein Skript (`/app/connector-superoffice/generate_customer_product_report.py`) wurde entwickelt, das diese Probleme adressiert. Es fragt gezielt nur `Sale`-Objekte ab, die eine gültige `Contact`-Verknüpfung besitzen (`$filter=Contact ne null`). Anschließend extrahiert es den Kundennamen und das `Heading`-Feld des Verkaufs und durchsucht letzteres nach vordefinierten Produkt-Schlüsselwörtern. Die Ergebnisse werden für die manuelle Analyse in einer CSV-Datei (`product_report.csv`) gespeichert. Dieser Ansatz ist der einzig verlässliche Weg, um die gewünschten Informationen aus dem System zu extrahieren.
### 7. Service & Tickets (Anfragen)
SuperOffice Tickets represent the support and request system. Like Sales, they are organized to allow separation between Roboplanet and Wackler.

View File

@@ -1,352 +1,40 @@
# SuperOffice Connector & GTM Engine ("The Muscle & The Brain")
Dieses Dokument beschreibt die Architektur der **Go-to-Market (GTM) Engine**, die SuperOffice CRM mit der Company Explorer Intelligence verbindet.
Ziel des Systems ist der vollautomatisierte Versand von **hyper-personalisierten E-Mails**, die so wirken, als wären sie manuell von einem Branchenexperten geschrieben worden.
---
## 1. Das Konzept: "Static Magic"
Anders als bei üblichen KI-Tools, die E-Mails "on the fly" generieren, setzt dieses System auf **vorberechnete, statische Textbausteine**.
**Warum?**
1. **Qualitätssicherung:** Jeder Baustein kann vor dem Versand geprüft werden.
2. **Performance:** SuperOffice muss beim Versand keine KI anfragen, sondern nur Felder zusammenfügen.
3. **Konsistenz:** Ein "Finanzleiter im Maschinenbau" bekommt immer dieselbe, perfekte Argumentation egal bei welchem Unternehmen.
### Die E-Mail-Formel
Eine E-Mail setzt sich aus **drei statischen Komponenten** zusammen, die im CRM (SuperOffice) gespeichert sind:
```text
[1. Opener (Unternehmens-Spezifisch)] + [2. Bridge (Persona x Vertical)] + [3. Social Proof (Vertical)]
```
* **1. Opener (Der Haken):** Bezieht sich zu 100% auf das spezifische Unternehmen und dessen Geschäftsmodell.
* *Quelle:* `Company`-Objekt (Feld: `ai_opener`).
* *Beispiel:* "Die präzise Just-in-Time-Fertigung von **Müller CNC** erfordert einen reibungslosen Materialfluss ohne Mikrostillstände."
* **2. Bridge (Die Relevanz):** Holt die Person in ihrer Rolle ab und verknüpft sie mit dem Branchen-Pain.
* *Quelle:* `Matrix`-Tabelle (Feld: `intro`).
* *Beispiel:* "Für Sie als **Produktionsleiter** bedeutet das, trotz Fachkräftemangel die Taktzeiten an der Linie stabil zu halten."
* **3. Social Proof (Die Lösung):** Zeigt Referenzen und den konkreten Nutzen (Gains).
* *Quelle:* `Matrix`-Tabelle (Feld: `social_proof`).
* *Beispiel:* "Unternehmen wie **Jungheinrich** nutzen unsere Transportroboter, um Fachkräfte an der Maschine zu halten und Suchzeiten um 30% zu senken."
---
## 2. Die Datenbasis (Foundation)
Die Qualität der Texte steht und fällt mit der Datenbasis. Diese wird zentral in **Notion** gepflegt und in den Company Explorer synchronisiert.
### A. Verticals (Branchen)
Definiert die **Makro-Pains** und **Gains** einer Branche sowie das **passende Produkt**.
* *Beispiel:* Healthcare -> Pain: "Pflegekräfte machen Logistik" -> Gain: "Hände fürs Bett" -> Produkt: Service-Roboter.
* *Wichtig:* Unterscheidung nach **Ops-Focus** (Operativ vs. Infrastruktur) steuert das Produkt (Reinigung vs. Service).
### B. Personas (Rollen)
Definiert die **persönlichen Pains** einer Rolle.
* *Beispiel:* Produktionsleiter -> Pain: "OEE / Taktzeit".
* *Beispiel:* Geschäftsführer -> Pain: "ROI / Amortisation".
---
## 3. Die Matrix-Engine (Multiplikation)
Das Skript `generate_matrix.py` (im Backend) ist das Herzstück. Es berechnet **alle möglichen Kombinationen** aus Verticals und Personas voraus.
**Logik:**
1. Lade alle Verticals (`V`) und Personas (`P`).
2. Für jede Kombination `V x P`:
* Lade `V.Pains` und `P.Pains`.
* Generiere via Gemini einen **perfekten Satz 2 (Bridge)** und **Satz 3 (Proof)**.
* Generiere ein **Subject**, das den Persona-Pain trifft.
3. Speichere das Ergebnis in der Tabelle `marketing_matrix`.
*Ergebnis:* Eine Lookup-Tabelle, aus der für jeden Kontakt sofort der passende Text gezogen werden kann.
---
## 4. Der "Opener" (First Sentence)
Dieser Baustein ist der einzige, der **pro Unternehmen** generiert wird (bei der Analyse/Discovery).
**Logik:**
1. Scrape Website-Content.
2. Identifiziere das **Vertical** (z.B. Maschinenbau).
3. Lade den **Core-Pain** des Verticals (z.B. "Materialfluss").
4. **Prompt:** "Analysiere das Geschäftsmodell von [Firma]. Formuliere einen Satz, der erklärt, warum [Core-Pain] für genau dieses Geschäftsmodell kritisch ist."
*Ergebnis:* Ein Satz, der beweist: "Ich habe verstanden, was ihr tut."
---
## 5. SuperOffice Connector ("The Muscle")
Der Connector ist der Bote, der diese Daten in das CRM bringt.
**Workflow:**
1. **Trigger:** Kontakt-Änderung in SuperOffice (Webhook).
2. **Enrichment:** Connector fragt Company Explorer: "Gib mir Daten für Firma X, Person Y".
3. **Lookup:** Company Explorer...
* Holt den `Opener` aus der Company-Tabelle.
* Bestimmt `Vertical` und `Persona`.
* Sucht den passenden Eintrag in der `MarketingMatrix`.
4. **Write-Back:** Connector schreibt die Texte in die UDF-Felder (User Defined Fields) des Kontakts in SuperOffice.
* `UDF_Opener`
* `UDF_Bridge`
* `UDF_Proof`
* `UDF_Subject`
* `UDF_UnsubscribeLink`
---
### 6. Monitoring & Dashboard ("The Eyes")
Das System verfügt über ein integriertes Echtzeit-Dashboard zur Überwachung der Synchronisationsprozesse.
**Features:**
* **Account-basierte Ansicht:** Gruppiert alle Ereignisse nach SuperOffice-Account oder Person, um den aktuellen Status pro Datensatz zu zeigen.
* **Phasen-Visualisierung:** Stellt den Fortschritt in vier Phasen dar:
1. **Received:** Webhook erfolgreich empfangen.
2. **Enriching:** Datenanreicherung im Company Explorer läuft (Gelb blinkend = In Arbeit).
3. **Syncing:** Rückschreiben der Daten nach SuperOffice (Gelb blinkend = In Arbeit).
4. **Completed:** Prozess für diesen Kontakt erfolgreich abgeschlossen (Grün).
* **Performance-Tracking:** Anzeige der Gesamtdurchlaufzeit (Duration) pro Prozess.
* **Fehler-Analyse:** Detaillierte Fehlermeldungen direkt in der Übersicht.
* **Dark Mode:** Modernes UI-Design für Admin-Monitoring.
**Zugriff:**
Das Dashboard ist über das Company Explorer Frontend (Icon "Activity" im Header) oder direkt unter `/connector/dashboard` erreichbar.
---
## 7. Setup & Wartung
### Neue Branche hinzufügen
1. In **Notion** anlegen (Pains/Gains/Produkte definieren).
2. Sync-Skript laufen lassen: `python3 backend/scripts/sync_notion_industries.py`.
3. Matrix neu berechnen: `python3 backend/scripts/generate_matrix.py --live`.
### End-to-End Tests
Ein automatisierter Integrationstest (`tests/test_e2e_flow.py`) deckt den gesamten Zyklus ab:
1. **Company Creation:** Webhook -> CE Provisioning -> Write-back (Vertical).
2. **Person Creation:** Webhook -> CE Matrix Lookup -> Write-back (Texte).
3. **Vertical Change:** Änderung im CRM -> CE Update -> Cascade zu Personen -> Neue Texte.
Ausführen mittels:
```bash
python3 connector-superoffice/tests/test_e2e_flow.py
```
## 7. Troubleshooting & Known Issues
### Authentication "URL has an invalid label"
Tritt auf, wenn `SO_ENVIRONMENT` leer ist. Der Client fällt nun automatisch auf `sod` zurück.
### Pydantic V2 Compatibility
Die `config.py` wurde auf natives Python (`os.getenv`) umgestellt, um Konflikte mit `pydantic-settings` in Docker-Containern zu vermeiden.
### Address & VAT Sync (WIP)
Der Worker wurde erweitert, um auch `City` und `OrgNumber` (VAT) zurückzuschreiben.
**Status (21.02.2026):** Implementiert, aber noch im Feinschliff. Logs zeigen teils Re-Queueing während das Enrichment läuft.
### 8. Lessons Learned: Address & VAT Sync (Solved Feb 22, 2026)
Die Synchronisation von Adressdaten stellte sich als unerwartet komplex heraus. Hier die wichtigsten Erkenntnisse für zukünftige Entwickler:
1. **Das "OrgNumber"-Phantom:**
* **Problem:** In der API-Dokumentation oft als `OrgNumber` referenziert, akzeptiert die WebAPI (REST) strikt nur **`OrgNr`**.
* **Lösung:** Mapping im `worker.py` hart auf `OrgNr` umgestellt.
2. **Verschachtelte Adress-Struktur:**
* **Problem:** Ein flaches Update auf `PostalAddress` wird von der API stillschweigend ignoriert (HTTP 200, aber keine Änderung).
* **Lösung:** Das Update muss die tiefe Struktur respektieren: `Address["Postal"]["City"]` UND `Address["Street"]["City"]`. Beide müssen explizit gesetzt werden, um in der UI sichtbar zu sein.
3. **Die "Race Condition" Falle (Atomic Updates):**
* **Problem:** Wenn Adress-Daten (`PUT Contact`) und UDF-Daten (`PUT Contact/Udef`) in separaten API-Aufrufen kurz hintereinander gesendet werden, gewinnt der letzte Call. Da dieser oft auf einem *veralteten* `GET`-Stand basiert (bevor das erste Update durch war), wurde die Adresse wieder mit "Leer" überschrieben.
* **Lösung:** **Atomic Update Strategy**. Der Worker sammelt *alle* Änderungen (Adresse, VAT, Vertical, Openers) in einem einzigen Dictionary und sendet genau **einen** `PUT`-Request an den Kontakt-Endpunkt. Dies garantiert Konsistenz.
---
### 9. Lessons Learned: Appointment Simulation & Persona Matching
Die Simulation von E-Mails via Terminen (Appointments) erforderte Workarounds für das UI-Verhalten von SuperOffice.
1. **Header vs. Description (Die 42-Zeichen-Grenze):**
* SuperOffice nutzt im Kalender und in Listen den `MainHeader` als Titel. Dieser ist auf ca. 42 Zeichen begrenzt.
* Ist der Titel länger, schneidet SuperOffice ihn ab. Erscheint der Titel inkonsistent, "stiehlt" das UI oft die erste Zeile der `Description` als Titel-Ersatz.
* **Strategie:** Wir kürzen den `MainHeader` auf 40 Zeichen und stellen sicher, dass der **vollständige Betreff** als allererste Zeile in der `Description` steht. Danach folgen zwei Newlines. Damit landet der Betreff im UI-Header und die Anrede ("Hallo...") bleibt sicher im Textkörper.
2. **Mapping-Resilienz (Funktion vs. Titel):**
* Jobtitel (Funktion) landen in SuperOffice inkonsistent in den Feldern `JobTitle` oder `Title`.
* **Lösung:** Der Worker fragt nun beide Felder ab (`person.get("JobTitle") or person.get("Title")`), um die Rolle korrekt zuzuweisen.
3. **Rollen-Dynamik:**
* Um zu verhindern, dass alte Rollen (z.B. "Infrastruktur") nach einer Beförderung/Änderung in SuperOffice "kleben" bleiben, führt das System nun bei jeder Namens- oder Funktionsänderung einen **Rollen-Reset** durch.
---
### 10. Lessons Learned: API Optimization & Certification (Feb 24, 2026)
Um die Zertifizierung für den SuperOffice App Store zu erhalten, mussten kritische Performance-Optimierungen durchgeführt werden.
1. **Die `getAllRows`-Falle:**
* **Problem:** SuperOffice monierte in der Validierung API-Calls wie `getAllRows` (implizit oft durch Abfragen ganzer Objekte ohne Filter), die unnötige Last verursachen.
* **Lösung:** Implementierung von **OData `$select`**. Wir fordern nun strikt nur die Felder an, die wir wirklich benötigen (z.B. `get_person(id, select=['JobTitle', 'UserDefinedFields'])`).
* **Wichtig:** Niemals pauschal `get_person()` aufrufen, wenn nur die Rolle geprüft werden soll.
3. **PUT vs. PATCH (Safe Updates):**
* **Problem:** Die Verwendung von `PUT` zum Aktualisieren von Entitäten (Person/Contact) erfordert, dass das *gesamte* Objekt gesendet wird. Dies birgt das Risiko, Felder zu überschreiben, die zwischenzeitlich von anderen Benutzern geändert wurden ("Race Condition"), und verursacht unnötigen Traffic.
* **Lösung:** Umstellung auf **`PATCH`**. Wir senden nun nur noch die *tatsächlich geänderten Felder* (Delta).
* **Implementierung:** Der Worker baut nun ein `patch_payload` (z.B. `{'Position': {'Id': 123}}`) und nutzt den dedizierten PATCH-Endpunkt. Dies wurde explizit von SuperOffice für die Zertifizierung gefordert.
### 11. Production Environment (Live Feb 27, 2026)
Nach erfolgreicher Zertifizierung durch SuperOffice wurde der Connector auf die Produktionsumgebung umgestellt.
* **Tenant:** `Cust26720`
* **Environment:** `online3` (zuvor `sod`)
* **Endpoint:** `https://online3.superoffice.com/Cust26720/api/v1`
* **Authentication:** Umstellung auf Produktions-Client-ID und -Secret erfolgreich verifiziert (Health Check OK).
**Wichtig:** SuperOffice nutzt Load-Balancing. Die Subdomain (`online3`) kann sich theoretisch ändern. Die Anwendung prüft dies dynamisch, aber die Basis-Konfiguration sollte den aktuellen Tenant-Status widerspiegeln.
### 12. Lessons Learned: Production Migration (Feb 27, 2026)
Der Wechsel von der Staging-Umgebung (`sod`) zur Produktion (`onlineX`) brachte spezifische technische Hürden mit sich:
1. **Globaler Token-Endpunkt:**
* **Problem:** Mandantenspezifische Subdomains (wie `online3.superoffice.com`) akzeptieren oft keine OAuth-Anfragen oder liefern leere Antworten.
* **Lösung:** Für den Token-Refresh muss zwingend der globale Endpunkt **`https://online.superoffice.com/login/common/oauth/tokens`** verwendet werden, unabhängig davon, auf welchem Cluster der Mandant liegt.
2. **DNS-Präfixe (app- vs. direkt):**
* **Problem:** In der Staging-Umgebung lautet der API-Host meist `app-sod.superoffice.com`. In der Produktion wird das `app-` Präfix oft nicht verwendet oder führt zu Zertifikatsfehlern.
* **Lösung:** Der `SuperOfficeClient` wurde so flexibilisiert, dass er in der Produktion direkt auf `{env}.superoffice.com` zugreift.
3. **Environment Variables Persistence:**
* **Problem:** Docker-Container behalten Umgebungsvariablen oft im Cache ("Shadow Configuration"), selbst wenn die `.env`-Datei geändert wurde.
* **Lösung:** Zwingendes `docker-compose up -d --force-recreate` nach Credentials-Änderungen.
### 13. Post-Migration Configuration (Cust26720)
Die Konfiguration in der `.env` Datei wurde für die Produktion wie folgt finalisiert:
| Funktion | UDF / ID | Entity |
| :--- | :--- | :--- |
| **Subject** | `SuperOffice:19` | Person |
| **Intro Text** | `SuperOffice:20` | Person |
| **Social Proof** | `SuperOffice:21` | Person |
| **Unsubscribe** | `SuperOffice:22` | Person |
| **Campaign Tag** | `SuperOffice:23` | Person |
| **Opener Primary** | `SuperOffice:86` | Contact |
| **Opener Sec.** | `SuperOffice:87` | Contact |
| **Vertical** | `SuperOffice:83` | Contact |
| **Summary** | `SuperOffice:84` | Contact |
| **Last Update** | `SuperOffice:85` | Contact |
### 14. Kampagnen-Steuerung (Usage)
Das System unterstützt mehrere Outreach-Varianten über das Feld **`MA_Campaign`** (Person).
1. **Standard:** Bleibt das Feld leer, werden die Standard-Texte ("standard") für Kaltakquise geladen.
2. **Spezifisch:** Wird ein Wert gewählt (z.B. "Messe 2026"), sucht der Connector gezielt nach Matrix-Einträgen mit diesem Tag.
3. **Fallback:** Existiert für die gewählte Kampagne kein spezifischer Text für das Vertical/Persona, wird automatisch auf "standard" zurückgegriffen.
### 16. Email Sending Implementation (Feb 28, 2026)
A dedicated script `create_email_test.py` has been implemented to create "Email Documents" directly in SuperOffice via the API. This bypasses the need for an external SMTP server by utilizing SuperOffice's internal document system.
**Features:**
* **Document Creation:** Creates a document of type "Ausg. E-Mail" (Template ID 157).
* **Activity Tracking:** Automatically creates a linked "Appointment" (Task ID 6 - Document Out) to ensure the email appears in the contact's activity timeline.
* **Direct Link:** Outputs a direct URL to open the created document in SuperOffice Online.
**Usage:**
```bash
python3 connector-superoffice/create_email_test.py <PersonID>
# Example:
python3 connector-superoffice/create_email_test.py 193036
```
**Key API Endpoints Used:**
* `POST /Document`: Creates the email body and metadata.
* `POST /Appointment`: Creates the activity record linked to the document.
### 17. Sales & Opportunities Implementation (Feb 28, 2026)
We have successfully prototyped the creation of "Sales" (Opportunities) via the API. This allows the AI to not only enrich contacts but also create tangible pipeline objects for sales representatives.
**Prototype Script:** `connector-superoffice/create_sale_test.py`
**Key Findings (Roboplanet Specific):**
1. **Authentication:** Must use `load_dotenv(override=True)` to ensure production credentials are used (see GEMINI.md).
2. **Sale Type (CRITICAL):** Use `SaleType: { "Id": 14 }` (**Roboplanet Verkauf**) to separate from Wackler parent data.
3. **Mandatory Fields:**
* `Heading`: Title of the opportunity.
* `Saledate`: Estimated closing date (ISO format).
* `Amount` & `Currency`: Use `Currency: { "Id": 33 }` (EUR).
* `Stage`: `{ "Id": 10 }` (5% Prospect).
* `Contact`: `{ "ContactId": 123 }`.
**Usage:**
```bash
python3 connector-superoffice/create_sale_test.py
```
This script finds the first available contact in the CRM and creates a test opportunity for them.
### 18. Service & Request Management (Tickets)
The Service module handles incoming requests and support cases. For Roboplanet, specific categories are used to manage leads and partner interactions.
**Key Categories (Roboplanet):**
* **Lead Roboplanet (ID 46):** Incoming potential leads.
* **Vertriebspartner Roboplanet (ID 47):** Communication with partners.
* **Weitergabe Roboplanet (ID 48):** Internal handovers.
**Data Structure (Entity: `ticket`):**
Tickets are linked via `contactId` and `personId`. They can also be associated with a `saleId` to provide a 360-degree view of a deal including its technical support questions.
### 19. Product Catalog & Families
SuperOffice contains a shared product catalog. Roboplanet's hardware portfolio is mapped directly to **Product Families**.
**Key Product Families (Robot Models):**
* **Cleaning:**
* `CC1` (ID 17), `CC1 Pro` (ID 34)
* `Scrubber 50` (ID 24), `Scrubber 75` (ID 25)
* `Nexaro 1500` (ID 26)
* `Phantas` (ID 23)
* **Service/Transport:**
* `BellaBot` (ID 18), `BellaBot Pro` (ID 19)
* `KettyBot Pro` (ID 22)
* `HolaBot` (ID 29)
* `FlashBot` (ID 21)
**Usage:** When creating Quote Lines (`Quote/Line`), linking to these families helps in reporting pipeline value by robot model.
### 20. Selections & Target Lists
Selections are the primary tool for grouping contacts in SuperOffice (e.g. for mailings or call lists).
* **API Access:** `/Selection` endpoints allow reading and adding members.
* **Strategy:** Instead of just tagging contacts via UDFs, adding them to a specific **Static Selection** (e.g., "AI Generated Leads - Review Pending") makes them immediately actionable for sales reps in their "Selections" tab.
---
This is the core logic used to generate the company-specific opener.
**Goal:** Prove understanding of the business model + imply the pain (positive observation).
```text
Du bist ein exzellenter B2B-Stratege und Texter mit einem tiefen Verständnis für operative Prozesse.
Deine Aufgabe ist es, einen hochpersonalisierten, scharfsinnigen und wertschätzenden Einleitungssatz für eine E-Mail an ein potenzielles Kundenunternehmen zu formulieren.
--- Denkprozess & Stilvorgaben ---
1. **Analysiere den Kontext:** Verstehe das Kerngeschäft. Was ist die kritische, physische Tätigkeit vor Ort? (z.B. 'Betrieb von Hochregallagern', 'Pflege von Patienten').
2. **Identifiziere den Hebel:** Was ist der Erfolgsfaktor? (z.B. 'reibungslose Abläufe', 'maximale Hygiene').
3. **Formuliere den Satz (ca. 20-35 Wörter):**
- Wähle einen eleganten, aktiven Einstieg wie 'Speziell im Bereich...' oder 'Der reibungslose Betrieb...'.
- Verbinde die **spezifische Tätigkeit** mit dem **Hebel** und den **geschäftlichen Konsequenzen**.
- **WICHTIG:** Formuliere immer als positive Beobachtung über eine Kernkompetenz. Du implizierst die Herausforderung durch die Betonung der Wichtigkeit.
- **VERMEIDE:** Konkrete Zahlen (z.B. "35 Rutschen"), da diese veraltet sein können. Nutze abstrakte Größen ("weitläufige Anlagen").
```
# SuperOffice Connector README
## Overview
This directory contains Python scripts designed to integrate with the SuperOffice CRM API, primarily for data extraction and analysis related to sales and customer product information.
## Authentication
Authentication is handled via the `AuthHandler` class, which uses a refresh token flow to obtain access tokens. Ensure that the `.env` file in the project root is correctly configured with `SO_CLIENT_ID`, `SO_CLIENT_SECRET`, `SO_REFRESH_TOKEN`, `SO_REDIRECT_URI`, `SO_ENVIRONMENT`, and `SO_CONTEXT_IDENTIFIER`.
## Key SuperOffice Entities and Data Model Observations
During the development of reporting functionalities, the following observations were made regarding the SuperOffice data model:
### 1. Sale Entity
- **Primary Source for Product Information:** Contrary to initial expectations, product information is frequently stored as free-text within the `Heading` field of the `Sale` object (e.g., `"2xOmnie CD-01 mit Nachlass"`). This appears to be a common practice in the system, rather than utilizing structured product catalog items linked via quotes.
- **Contact Association:** A significant number of `Sale` objects (`Sale?$filter=Contact ne null`) are not directly linked to a `Contact` object (`Contact: null`), making it challenging to attribute sales to specific customers programmatically. Our reporting scripts specifically filter for sales where a `Contact` is present.
- **No Direct Quote/QuoteLine Linkage:** Attempts to retrieve `Quote` or `QuoteLine` objects directly via `Sale/{saleId}/Quotes`, `Contact/{contactId}/Quotes`, or `Sale/{saleId}/Activities` resulted in `500 Internal Server Errors` or empty result sets. This indicates that direct, API-accessible linkages between `Sales` and `structured QuoteLines` are often absent or not exposed via these endpoints.
### 2. Product Information Storage (Hypothesis & Workaround)
- **Free-Text in Heading:** The primary source for identifying products associated with a sale is the `Heading` field of the `Sale` entity itself. This field often contains product codes, descriptions, and other relevant details as free-text.
- **User-Defined Fields (UDFs):** While `UserDefinedFields` were inspected for structured product data (e.g., `RR-02-017-OMNIE`), no such patterns were found in the `sale_id=342243` example. This suggests that UDFs are either not consistently used for product codes or are named in a way that doesn't align with common product terminology.
## Scripts
### `list_products.py`
- **Purpose:** Fetches and displays a list of all defined product families from the SuperOffice product catalog (`/List/ProductFamily/Items`).
- **Usage:** `python3 list_products.py`
### `generate_customer_product_report.py`
- **Purpose:** Generates a CSV report of customer sales, extracting product information from the `Sale.Heading` field using keyword matching.
- **Methodology:**
1. Retrieves the latest `SALE_LIMIT` (e.g., 1000) `Sale` objects, filtering only those with an associated `Contact` (`$filter=Contact ne null`).
2. Extracts `SaleId`, `CustomerName`, and `SaleHeading` for each relevant sale.
3. Searches the `SaleHeading` for predefined `PRODUCT_KEYWORDS` (e.g., `OMNIE`, `CD-01`, `Service`).
4. Outputs the results to `product_report.csv`.
- **Usage:** `python3 generate_customer_product_report.py`
## Future Work
- **Analyse der leeren `product_report.csv`:** Untersuchen, warum die `product_report.csv` auch nach der Filterung nach `Sale`-Objekten mit `Contact`-Verknüpfung leer bleibt. Es ist entscheidend zu verstehen, ob es keine solchen Verkäufe gibt oder ob ein Problem mit der Datenabfrage oder -verarbeitung vorliegt.
- **Manuelle Inspektion gefilterter `Sale`-Objekte:** Wenn der Report leer ist, müssen wir einige `Sale`-Objekte, die die Bedingung `Contact ne null` erfüllen, manuell inspizieren, um ihre Struktur zu verstehen und festzustellen, ob das `Heading`-Feld oder andere Felder Produktinformationen enthalten.
- **Verfeinerung der `PRODUCT_KEYWORDS`:** Die Liste der Produkt-Schlüsselwörter muss möglicherweise erweitert werden, basierend auf einer detaillierteren manuellen Analyse der Verkaufsüberschriften.
- **Erforschung alternativer API-Pfade:** Falls der aktuelle Ansatz weiterhin Schwierigkeiten bereitet, müssen wir tiefer in die SuperOffice-API eintauchen, um strukturierte Produktdaten zu finden, auch wenn sie nicht direkt mit den Verkäufen verknüpft sind.

View File

@@ -0,0 +1,126 @@
import os
import csv
import logging
import requests
import json
from dotenv import load_dotenv
# --- Configuration ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("customer_product_report")
OUTPUT_FILE = 'product_report.csv'
SALE_LIMIT = 1000 # Process the top 1000 most recently updated sales
PRODUCT_KEYWORDS = [
'OMNIE', 'CD-01', 'RR-02-017', 'Service', 'Dienstleistung',
'Wartung', 'Support', 'Installation', 'Beratung'
]
# --- Auth & API Client Classes (from previous scripts) ---
class AuthHandler:
def __init__(self):
load_dotenv(override=True)
self.client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
self.client_secret = os.getenv("SO_CLIENT_SECRET")
self.refresh_token = os.getenv("SO_REFRESH_TOKEN")
self.redirect_uri = os.getenv("SO_REDIRECT_URI", "http://localhost")
self.env = os.getenv("SO_ENVIRONMENT", "sod")
self.cust_id = os.getenv("SO_CONTEXT_IDENTIFIER", "Cust55774")
if not all([self.client_id, self.client_secret, self.refresh_token]):
raise ValueError("SuperOffice credentials missing in .env file.")
def get_access_token(self):
return self._refresh_access_token()
def _refresh_access_token(self):
token_domain = "online.superoffice.com" if "online" in self.env.lower() else "sod.superoffice.com"
url = f"https://{token_domain}/login/common/oauth/tokens"
data = {"grant_type": "refresh_token", "client_id": self.client_id, "client_secret": self.client_secret, "refresh_token": self.refresh_token, "redirect_uri": self.redirect_uri}
try:
resp = requests.post(url, data=data)
resp.raise_for_status()
return resp.json().get("access_token")
except requests.RequestException as e:
logger.error(f"❌ Connection Error during token refresh: {e}")
return None
class SuperOfficeClient:
def __init__(self, auth_handler):
self.auth_handler = auth_handler
self.base_url = f"https://{self.auth_handler.env}.superoffice.com/{self.auth_handler.cust_id}/api/v1"
self.access_token = self.auth_handler.get_access_token()
if not self.access_token:
raise Exception("Failed to obtain access token.")
self.headers = {"Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json", "Accept": "application/json"}
def _get(self, endpoint):
url = f"{self.base_url}/{endpoint}"
logger.debug(f"GET: {url}")
resp = requests.get(url, headers=self.headers)
if resp.status_code == 204: return None
resp.raise_for_status()
return resp.json()
def find_keywords(text):
"""Searches for keywords in a given text, case-insensitively."""
found = []
if not text:
return found
text_lower = text.lower()
for keyword in PRODUCT_KEYWORDS:
if keyword.lower() in text_lower:
found.append(keyword)
return found
def main():
logger.info("--- Starting Customer Product Report Generation ---")
try:
auth = AuthHandler()
client = SuperOfficeClient(auth)
# 1. Fetch the most recently updated sales
logger.info(f"Fetching the last {SALE_LIMIT} updated sales...")
# OData query to get the top N sales that have a contact associated
sales_endpoint = f"Sale?$filter=Contact ne null&$orderby=saleId desc&$top={SALE_LIMIT}&$select=SaleId,Heading,Contact"
sales_response = client._get(sales_endpoint)
if not sales_response or 'value' not in sales_response:
logger.warning("No sales with associated contacts found.")
return
sales = sales_response['value']
logger.info(f"Found {len(sales)} sales with associated contacts to process.")
# Removed the debug log to avoid excessive output of the same data
# 2. Process each sale and write to CSV
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8') as csvfile:
fieldnames = ['SaleID', 'CustomerName', 'SaleHeading', 'DetectedKeywords']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for sale in sales:
if not sale.get('Contact') or not sale['Contact'].get('ContactId'):
logger.warning(f"Skipping Sale {sale.get('SaleId')} because it has no linked Contact.")
continue
sale_id = sale.get('SaleId')
heading = sale.get('Heading', 'N/A')
customer_name = sale['Contact'].get('Name', 'N/A')
# Find keywords in the heading
keywords_found = find_keywords(heading)
writer.writerow({
'SaleID': sale_id,
'CustomerName': customer_name,
'SaleHeading': heading,
'DetectedKeywords': ', '.join(keywords_found) if keywords_found else 'None'
})
logger.info(f"--- ✅ Report generation complete. ---")
logger.info(f"Results saved to '{OUTPUT_FILE}'.")
except requests.exceptions.HTTPError as e:
logger.error(f"❌ API Error: {e.response.status_code} - {e.response.text}")
except Exception as e:
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,122 @@
import os
import sys
import json
import logging
import requests
from dotenv import load_dotenv
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("list_products")
# --- Inline AuthHandler & SuperOfficeClient (Proven Working Logic) ---
class AuthHandler:
def __init__(self):
# CRITICAL: override=True ensures we read from .env even if env vars are already set
load_dotenv(override=True)
self.client_id = os.getenv("SO_CLIENT_ID") or os.getenv("SO_SOD")
self.client_secret = os.getenv("SO_CLIENT_SECRET")
self.refresh_token = os.getenv("SO_REFRESH_TOKEN")
self.redirect_uri = os.getenv("SO_REDIRECT_URI", "http://localhost")
self.env = os.getenv("SO_ENVIRONMENT", "sod")
self.cust_id = os.getenv("SO_CONTEXT_IDENTIFIER", "Cust55774")
if not all([self.client_id, self.client_secret, self.refresh_token]):
raise ValueError("SuperOffice credentials missing in .env file.")
def get_access_token(self):
return self._refresh_access_token()
def _refresh_access_token(self):
token_domain = "online.superoffice.com" if "online" in self.env.lower() else "sod.superoffice.com"
url = f"https://{token_domain}/login/common/oauth/tokens"
data = {
"grant_type": "refresh_token",
"client_id": self.client_id,
"client_secret": self.client_secret,
"refresh_token": self.refresh_token,
"redirect_uri": self.redirect_uri
}
try:
resp = requests.post(url, data=data)
if resp.status_code != 200:
logger.error(f"❌ Token Refresh Failed (Status {resp.status_code}): {resp.text}")
return None
return resp.json().get("access_token")
except Exception as e:
logger.error(f"❌ Connection Error during token refresh: {e}")
return None
class SuperOfficeClient:
def __init__(self, auth_handler):
self.auth_handler = auth_handler
self.env = auth_handler.env
self.cust_id = auth_handler.cust_id
self.base_url = f"https://{self.env}.superoffice.com/{self.cust_id}/api/v1"
self.access_token = self.auth_handler.get_access_token()
if not self.access_token:
raise Exception("Failed to obtain access token.")
self.headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
def _get(self, endpoint):
url = f"{self.base_url}/{endpoint}"
logger.info(f"Attempting to GET: {url}")
resp = requests.get(url, headers=self.headers)
resp.raise_for_status()
return resp.json()
def main():
try:
# Initialize Auth and Client
auth = AuthHandler()
client = SuperOfficeClient(auth)
print("\n--- 1. Fetching Product Information ---")
product_families = []
endpoint_to_try = "List/ProductFamily/Items"
try:
product_families = client._get(endpoint_to_try)
except requests.exceptions.HTTPError as e:
logger.error(f"Failed to fetch from '{endpoint_to_try}': {e}")
print(f"Could not find Product Families at '{endpoint_to_try}'. This might not be the correct endpoint or list name.")
# If the first endpoint fails, you could try others here.
# For now, we will exit if this one fails.
return
if not product_families:
print("No product families found or the endpoint returned an empty list.")
return
print("\n--- ✅ SUCCESS: Found Product Families ---")
print("-----------------------------------------")
print(f"{'ID':<10} | {'Name':<30} | {'Tooltip':<40}")
print("-----------------------------------------")
for product in product_families:
product_id = product.get('Id', 'N/A')
name = product.get('Name', 'N/A')
tooltip = product.get('Tooltip', 'N/A')
print(f"{str(product_id):<10} | {name:<30} | {tooltip:<40}")
print("-----------------------------------------")
except requests.exceptions.HTTPError as e:
logger.error(f"❌ API Error: {e}")
if e.response is not None:
logger.error(f"Response Body: {e.response.text}")
except Exception as e:
logger.error(f"Fatal Error: {e}")
if __name__ == "__main__":
main()

2890
sale_342243_details.txt Normal file

File diff suppressed because it is too large Load Diff