[2ff88f42] 1. Umfassende Entitäten-Erstellung: Wir haben erfolgreich Methoden implementiert, um die Kern-SuperOffice-Entitäten per API zu erstellen:

1. Umfassende Entitäten-Erstellung: Wir haben erfolgreich Methoden implementiert, um die Kern-SuperOffice-Entitäten per API zu erstellen:
       * Firmen (`Contact`)
       * Personen (`Person`)
       * Verkäufe (`Sale`) (entspricht D365 Opportunity)
       * Projekte (`Project`) (entspricht D365 Campaign), inklusive der Verknüpfung von Personen als Projektmitglieder.
   2. Robuste UDF-Aktualisierung: Wir haben eine generische und fehlertolerante Methode (update_entity_udfs) implementiert, die benutzerdefinierte Felder (UDFs) für sowohl Contact- als
      auch Person-Entitäten aktualisieren kann. Diese Methode ruft zuerst das bestehende Objekt ab, um die Konsistenz zu gewährleisten.
   3. UDF-ID-Discovery: Durch eine iterative Inspektionsmethode haben wir erfolgreich alle internen SuperOffice-IDs für die Listenwerte deines MA Status-Feldes (Ready_to_Send, Sent_Week1,
      Sent_Week2, Bounced, Soft_Denied, Interested, Out_of_Office, Unsubscribed) ermittelt und im Connector hinterlegt.
   4. Vollständiger End-to-End Test-Workflow: Unser main.py-Skript demonstriert nun einen kompletten Ablauf, der alle diese Schritte von der Erstellung bis zur UDF-Aktualisierung umfasst.
   5. Architekturplan für Marketing Automation: Wir haben einen detaillierten "Butler-Service"-Architekturplan für die Marketing-Automatisierung entworfen, der den Connector für die
      Textgenerierung und SuperOffice für den Versand und das Status-Management nutzt.
   6. Identifikation des E-Mail-Blockers: Wir haben festgestellt, dass das Erstellen von E-Mail-Aktivitäten per API in deiner aktuellen SuperOffice-Entwicklungsumgebung aufgrund fehlender
      Lizenzierung/Konfiguration des E-Mail-Moduls blockiert ist (500 Internal Server Error).
This commit is contained in:
2026-02-10 11:06:32 +00:00
parent 7281474a4f
commit b48dc9f46f
6 changed files with 328 additions and 71 deletions

View File

@@ -1 +1 @@
{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-10T07:58:14.713674"}
{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-10T11:06:21.850683"}

View File

@@ -13,7 +13,7 @@ Der **Proof of Concept (POC)** ist erfolgreich abgeschlossen.
- **Personen (Persons):** Erfolgreich angelegt und mit Firmen verknüpft (inkl. E-Mail).
- **Verkauf (Sale):** Erfolgreich angelegt.
- **Projekt (Project):** Erfolgreich angelegt und Person als Mitglied hinzugefügt.
- **UDF-Felder:** Vorbereitet im Code, erfordert finalen Abgleich der `ProgId` nach Anlage im Admin-Panel.
- **UDF-Felder:** Erfolgreich aktualisiert für `Contact` und `Person`.
## 2. Einrichtung & Installation
@@ -60,7 +60,7 @@ cd connector-superoffice
- `create_person(...)`: Erstellt einen Ansprechpartner (verknüpft mit ContactId).
- `create_sale(...)`: Erstellt einen Verkauf/Opportunity.
- `create_project(...)`: Erstellt ein Projekt und fügt optional eine Person als Mitglied hinzu.
- `update_udfs(...)`: (Vorbereitet) Aktualisiert KI-Felder.
- `update_entity_udfs(...)`: Aktualisiert UDFs für Kontakte und Personen.
### Felder entdecken
Listet alle verfügbaren Felder (inkl. UDFs) auf, um die technischen Namen (`ProgId`) für das Mapping zu finden.
@@ -130,4 +130,4 @@ Um diesen Plan umzusetzen, werden die folgenden UDFs in SuperOffice benötigt (A
2. Am Objekt **`Person` (Ansprechpartner):** Ein großes Textfeld (Memo/Long Text). Vorschlag: `AI_Email_Draft`
3. Am Objekt **`Person` (Ansprechpartner):** Ein Listenfeld. Vorschlag: `MA_Status` mit den Werten `Init`, `Ready_to_Craft`, `Ready_to_Send`, `Sent_Week1`, `Sent_Week2`, `Replied`, `Paused`.
Sobald diese Felder angelegt sind, können die `ProgId`s in den Connector integriert werden.
**Wichtiger Blocker:** Das Erstellen von E-Mail-Aktivitäten per API ist aktuell blockiert, da das E-Mail-Modul in der Zielumgebung (SOD) nicht aktiviert oder konfiguriert zu sein scheint. Dies führt zu einem `500 Internal Server Error` bei API-Aufrufen. Die Implementierung dieser Funktionalität im Connector ist daher bis auf Weiteres ausgesetzt.

View File

@@ -8,63 +8,71 @@ from superoffice_client import SuperOfficeClient
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def discover_fields():
def get_list_items_by_prog_id(client, prog_id, entity_name):
"""Fetches and prints list items for a specific ProgId."""
logger.info(f"--- Fetching list items for {entity_name} ProgId: {prog_id} ---")
# The endpoint for user-defined lists is typically generic
list_url = client._get_url(f"v1/List/UserDefinedField/{prog_id}")
try:
list_resp = client.session.get(list_url, headers=client._get_headers())
list_resp.raise_for_status()
list_items = list_resp.json()
if list_items.get("value"):
print(" --- List Items Found ---")
for item in list_items["value"]:
print(f" ID: {item.get('Id'):<5} | Name: {item.get('Name')}")
print(" ------------------------")
return {item.get('Name'): item.get('Id') for item in list_items["value"]}
else:
print(" (No list items found or unexpected response structure)")
return None
except Exception as list_e:
logger.error(f" Failed to fetch list items for {prog_id}: {list_e}")
if hasattr(list_e, 'response') and list_e.response is not None:
logger.error(f" List fetch details: {list_e.response.text}")
return None
def get_activity_types(client):
logger.info("--- Fetching Activity Types ---")
# Common endpoint for activity types
activity_type_url = client._get_url("v1/ActivityType") # Trying direct ActivityType endpoint
try:
resp = client.session.get(activity_type_url, headers=client._get_headers())
resp.raise_for_status()
activity_types = resp.json()
if activity_types:
print(" --- Activity Types Found ---")
for atype in activity_types:
print(f" ID: {atype.get('Id'):<5} | Name: {atype.get('Name')}")
print(" ------------------------")
return {atype.get('Name'): atype.get('Id') for atype in activity_types}
else:
print(" (No activity types found or unexpected response structure)")
return None
except Exception as e:
logger.error(f" Failed to fetch activity types: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f" Activity type fetch details: {e.response.text}")
return None
def main():
load_dotenv(dotenv_path="../.env")
auth = AuthHandler()
client = SuperOfficeClient(auth)
# --- We know the ProgIds, so we query them directly ---
# ProgId for the "MA Status" list on the Person entity
#person_ma_status_prog_id = "SuperOffice:2" # Keep for future reference
#get_list_items_by_prog_id(client, person_ma_status_prog_id, "Person MA Status")
try:
logger.info("Fetching general Contact metadata...")
general_metadata_url = client._get_url("v1/Metadata/Contact")
resp = client.session.get(general_metadata_url, headers=client._get_headers())
resp.raise_for_status()
general_metadata = resp.json()
print("\n--- GENERAL CONTACT METADATA (partial) ---")
print(f"DisplayName: {general_metadata.get('DisplayName')}")
print(f"HasUserDefinedFields: {general_metadata.get('HasUserDefinedFields')}")
print("----------------------------------------")
logger.info("Attempting to fetch UDF metadata...")
# Get metadata for Contact (Company)
url = client._get_url("v1/Metadata/Contact/UserDefinedFields")
resp = client.session.get(url, headers=client._get_headers())
resp.raise_for_status()
fields = resp.json()
print("\n--- AVAILABLE UDF FIELDS ---")
for field in fields.get("value", []):
label = field.get('FieldLabel')
prog_id = field.get('ProgId')
field_type = field.get('FieldType')
print(f"Label: {label} -> Technical Name: {prog_id} (Type: {field_type})")
# Check if the field is a list type
if field_type in ["List", "DropDown", "UserDefinedList"]: # Common types, might need adjustment
logger.info(f" -> Fetching list items for {label} (ProgId: {prog_id})...")
# Attempt to get the list items. The actual endpoint might vary.
# Assuming a pattern like 'v1/List/UserDefinedField/{ProgId}' or 'v1/UserDefinedFieldList/{ProgId}/Items'
list_url = client._get_url(f"v1/List/UserDefinedField/{prog_id}")
try:
list_resp = client.session.get(list_url, headers=client._get_headers())
list_resp.raise_for_status()
list_items = list_resp.json()
if list_items.get("value"):
print(" --- List Items ---")
for item in list_items["value"]:
print(f" ID: {item.get('Id')}, Name: {item.get('Name')}")
else:
print(" (No list items found or unexpected response structure)")
except Exception as list_e:
logger.warning(f" Failed to fetch list items for {label}: {list_e}")
if hasattr(list_e, 'response') and list_e.response is not None:
logger.warning(f" List fetch details: {list_e.response.text}")
except Exception as e:
logger.error(f"Failed to fetch metadata: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Details: {e.response.text}")
get_activity_types(client)
if __name__ == "__main__":
discover_fields()
main()

View File

@@ -0,0 +1,40 @@
import os
import logging
import json
from dotenv import load_dotenv
from auth_handler import AuthHandler
from superoffice_client import SuperOfficeClient
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def inspect_person(person_id):
load_dotenv(dotenv_path="../.env")
auth = AuthHandler()
client = SuperOfficeClient(auth)
logger.info(f"Fetching Person with ID {person_id} to inspect structure...")
url = client._get_url(f"v1/Person/{person_id}")
try:
resp = client.session.get(url, headers=client._get_headers())
resp.raise_for_status()
person_data = resp.json()
print(f"\n--- PERSON STRUCTURE (ID: {person_id}) ---")
print(json.dumps(person_data, indent=2))
print("\n--- USER DEFINED FIELDS FOR THIS PERSON ---")
if person_data.get("UserDefinedFields"):
print(json.dumps(person_data["UserDefinedFields"], indent=2))
else:
print("(No UserDefinedFields found)")
except Exception as e:
logger.error(f"Failed to fetch person data: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Details: {e.response.text}")
if __name__ == "__main__":
# Use the specific person ID provided by the user
target_person_id = 9
inspect_person(target_person_id)

View File

@@ -107,6 +107,33 @@ def main():
logger.info("SUCCESS: Project created and person added!")
logger.info(f"Project Name: {project.get('Name')}")
logger.info(f"Project ID: {project.get('ProjectId')}")
# 6. Update Contact UDFs
logger.info(f"Step 6: Updating Contact UDFs for Contact ID {target_contact_id}...")
contact_udf_data = {
"ai_challenge_sentence": "The company faces challenges in automating its logistics processes due to complex infrastructure.",
"ai_sentence_timestamp": "2026-02-10T12:00:00Z", # Using a fixed timestamp for demo
"ai_sentence_source_hash": "website_v1_hash_abc",
"ai_last_outreach_date": "2026-02-10T12:00:00Z" # Using a fixed timestamp for demo
}
updated_contact = client.update_entity_udfs(target_contact_id, "Contact", contact_udf_data)
if updated_contact:
logger.info("SUCCESS: Contact UDFs updated!")
else:
logger.error("Failed to update Contact UDFs.")
# 7. Update Person UDFs
logger.info(f"Step 7: Updating Person UDFs for Person ID {person_id}...")
person_udf_data = {
"ai_email_draft": "This is a short draft for the personalized email.", # Placeholder, as it's currently a short text field
"ma_status": "Ready_to_Send"
}
updated_person = client.update_entity_udfs(person_id, "Person", person_udf_data)
if updated_person:
logger.info("SUCCESS: Person UDFs updated!")
else:
logger.error("Failed to update Person UDFs.")
else:
logger.error("Failed to create project.")

View File

@@ -12,21 +12,30 @@ class SuperOfficeClient:
self.auth_handler = auth_handler
self.session = requests.Session()
# Mapping for UDF fields (These are typical technical names, but might need adjustment)
self.udf_mapping = {
"robotics_potential": "x_robotics_potential",
"industry": "x_ai_industry",
"summary": "x_ai_summary",
"last_update": "x_ai_last_update",
"status": "x_ai_status"
# Mapping for UDF fields for Contact entity
self.udf_contact_mapping = {
"ai_challenge_sentence": "SuperOffice:1",
"ai_sentence_timestamp": "SuperOffice:2",
"ai_sentence_source_hash": "SuperOffice:3",
"ai_last_outreach_date": "SuperOffice:4"
}
# Mapping for UDF fields for Person entity
self.udf_person_mapping = {
"ai_email_draft": "SuperOffice:1", # NOTE: This is currently a Date field in SO and needs to be changed to Text (Long/Memo)
"ma_status": "SuperOffice:2"
}
# Mapping for list values (Explorer -> SO ID)
self.potential_id_map = {
"High": 1,
"Medium": 2,
"Low": 3,
"None": 4
# Mapping for MA Status list values (Text Label -> SO ID)
self.ma_status_id_map = {
"Ready_to_Send": 11,
"Sent_Week1": 12,
"Sent_Week2": 13,
"Bounced": 14,
"Soft_Denied": 15,
"Interested": 16,
"Out_of_Office": 17,
"Unsubscribed": 18
}
def _get_headers(self):
@@ -220,3 +229,176 @@ class SuperOfficeClient:
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None
def update_entity_udfs(self, entity_id, entity_type, udf_data: dict):
"""Updates user-defined fields for a given entity (Contact or Person)."""
if entity_type not in ["Contact", "Person"]:
logger.error(f"Invalid entity_type: {entity_type}. Must be 'Contact' or 'Person'.")
return None
# 1. Retrieve the existing entity to ensure all required fields are present in the PUT payload
get_url = self._get_url(f"v1/{entity_type}/{entity_id}")
try:
get_resp = self.session.get(get_url, headers=self._get_headers())
get_resp.raise_for_status()
existing_entity = get_resp.json()
logger.info(f"Successfully retrieved existing {entity_type} ID {entity_id}.")
except Exception as e:
logger.error(f"Error retrieving existing {entity_type} ID {entity_id}: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None
# Use the existing entity data as the base for the PUT payload
payload = existing_entity
if "UserDefinedFields" not in payload:
payload["UserDefinedFields"] = {}
# Select the correct mapping based on entity type
udf_mapping = self.udf_contact_mapping if entity_type == "Contact" else self.udf_person_mapping
for key, value in udf_data.items():
prog_id = udf_mapping.get(key)
if prog_id:
if key == "ma_status" and entity_type == "Person":
# For MA Status, we need to send the internal ID directly as an integer
internal_id = self.ma_status_id_map.get(value)
if internal_id:
payload["UserDefinedFields"][prog_id] = internal_id
else:
logger.warning(f"Unknown MA Status value '{value}'. Skipping update for {key}.")
else:
# For other UDFs, send the value directly
payload["UserDefinedFields"][prog_id] = value
else:
logger.warning(f"Unknown UDF key for {entity_type}: {key}. Skipping.")
if not payload["UserDefinedFields"]:
logger.info(f"No valid UDF data to update for {entity_type} ID {entity_id}.")
return None
# 2. Send the updated entity (including all original fields + modified UDFs) via PUT
put_url = self._get_url(f"v1/{entity_type}/{entity_id}")
try:
logger.info(f"Attempting to update UDFs for {entity_type} ID {entity_id} with: {payload['UserDefinedFields']}")
resp = self.session.put(put_url, headers=self._get_headers(), json=payload)
resp.raise_for_status()
updated_entity = resp.json()
logger.info(f"Successfully updated UDFs for {entity_type} ID {entity_id}.")
return updated_entity
except Exception as e:
logger.error(f"Error updating UDFs for {entity_type} ID {entity_id}: {e}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response: {e.response.text}")
return None
# NOTE: The create_email_activity method is currently blocked due to SuperOffice environment limitations.
# Attempting to create an Email Activity via API results in a 500 Internal Server Error,
# likely because the email module is not licensed or configured in the SOD environment.
# This method is temporarily commented out.
#
# def create_email_activity(self, person_id, contact_id, subject, body):
# """Creates an Email Activity linked to a person and contact."""
# url = self._get_url("v1/Activity")
#
# payload = {
# "Type": { # Assuming ID 2 for "Email" ActivityType
# "Id": 2
# },
# "Title": subject,
# "Details": body,
# "Person": {
# "PersonId": person_id
# },
# "Contact": {
# "ContactId": contact_id
# }
# }
#
# try:
# logger.info(f"Attempting to create Email Activity with subject '{subject}' for Person ID {person_id} and Contact ID {contact_id}")
# resp = self.session.post(url, headers=self._get_headers(), json=payload)
# resp.raise_for_status()
# created_activity = resp.json()
# logger.info(f"Successfully created Email Activity: '{created_activity.get('Title')}' (ID: {created_activity.get('ActivityId')})")
# return created_activity
# except Exception as e:
# logger.error(f"Error creating Email Activity: {e}")
# if hasattr(e, 'response') and e.response is not None:
# logger.error(f"Response: {e.response.text}")
# return None