From b236bbe29ca44444763ca1fec1c26fb80fa73887 Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 10 Feb 2026 11:06:32 +0000 Subject: [PATCH] =?UTF-8?q?[2ff88f42]=20=20=20=201.=20Umfassende=20Entit?= =?UTF-8?q?=C3=A4ten-Erstellung:=20Wir=20haben=20erfolgreich=20Methoden=20?= =?UTF-8?q?implementiert,=20um=20die=20Kern-SuperOffice-Entit=C3=A4ten=20p?= =?UTF-8?q?er=20API=20zu=20erstellen:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .dev_session/SESSION_INFO | 2 +- connector-superoffice/README.md | 6 +- connector-superoffice/discover_fields.py | 116 ++++++----- connector-superoffice/inspect_person.py | 40 ++++ connector-superoffice/main.py | 27 +++ connector-superoffice/superoffice_client.py | 208 ++++++++++++++++++-- 6 files changed, 328 insertions(+), 71 deletions(-) create mode 100644 connector-superoffice/inspect_person.py diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index d443b2a7..e027922b 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-10T07:58:14.713674"} \ No newline at end of file +{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-10T11:06:21.850683"} \ No newline at end of file diff --git a/connector-superoffice/README.md b/connector-superoffice/README.md index c1499088..0016186e 100644 --- a/connector-superoffice/README.md +++ b/connector-superoffice/README.md @@ -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. \ No newline at end of file +**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. \ No newline at end of file diff --git a/connector-superoffice/discover_fields.py b/connector-superoffice/discover_fields.py index 16485042..fb3438ad 100644 --- a/connector-superoffice/discover_fields.py +++ b/connector-superoffice/discover_fields.py @@ -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() \ No newline at end of file + main() \ No newline at end of file diff --git a/connector-superoffice/inspect_person.py b/connector-superoffice/inspect_person.py new file mode 100644 index 00000000..70995de5 --- /dev/null +++ b/connector-superoffice/inspect_person.py @@ -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) diff --git a/connector-superoffice/main.py b/connector-superoffice/main.py index 8de8d6b7..ebb9302d 100644 --- a/connector-superoffice/main.py +++ b/connector-superoffice/main.py @@ -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.") diff --git a/connector-superoffice/superoffice_client.py b/connector-superoffice/superoffice_client.py index ef692440..4ad0e8e6 100644 --- a/connector-superoffice/superoffice_client.py +++ b/connector-superoffice/superoffice_client.py @@ -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 + + + \ No newline at end of file