[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:
@@ -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.
|
||||
@@ -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()
|
||||
40
connector-superoffice/inspect_person.py
Normal file
40
connector-superoffice/inspect_person.py
Normal 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)
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user