From 2c132f5f47768f36dbdfbf5c81ad62d394b3ef7d Mon Sep 17 00:00:00 2001 From: Sarah Date: Tue, 6 Jun 2023 10:24:50 -0500 Subject: [PATCH 1/5] create template copy function --- scripts/clone_organization.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/scripts/clone_organization.py b/scripts/clone_organization.py index d7d8dff..017fd0f 100755 --- a/scripts/clone_organization.py +++ b/scripts/clone_organization.py @@ -101,6 +101,7 @@ from_organization = from_api.get("me")["organizations"][0] to_organization = to_api.get("me")["organizations"][0] +# ask user to confirm changes message = f"Cloning `{from_organization['name']}` ({from_organization['id']}) organization to `{to_organization['name']}` ({to_organization['id']})..." message += '\nData from source organization will be added to the destination organization. No data will be deleted.\n\nContinue?' @@ -108,6 +109,7 @@ if confirmed not in ["yes", "y"]: exit() +# Get lead statuses, remove old ids, then post to new org if args.lead_statuses or args.statuses or args.all: print("\nCopying Lead Statuses") @@ -121,6 +123,7 @@ except APIError as e: print(f"Couldn't add `{status['label']}` because {str(e)}") +# Copy pipelines and associated opportunity statuses if args.opportunity_statuses or args.statuses or args.all: print("\nCopying Opportunity Statuses") to_pipelines = to_api.get_opportunity_pipelines() @@ -256,31 +259,26 @@ def copy_custom_fields(custom_field_type): except APIError as e: print(f"Couldn't add `{role['name']}` because {str(e)}") -if args.templates or args.email_templates or args.all: - print("\nCopying Email Templates") - templates = from_api.get_all_items('email_template') +# function to copy sms or email templates +def copy_templates(template_type): + templates = from_api.get_all_items(template_type) for template in templates: del template["id"] del template["organization_id"] try: - to_api.post("email_template", data=template) + to_api.post(template_type, data=template) print(f'Added `{template["name"]}`') except APIError as e: print(f"Couldn't add `{template['name']}` because {str(e)}") +if args.templates or args.email_templates or args.all: + print("\nCopying Email Templates") + copy_templates('email_template') + if args.templates or args.sms_templates or args.all: print("\nCopying SMS Templates") - templates = from_api.get_all_items('sms_template') - for template in templates: - del template["id"] - del template["organization_id"] - - try: - to_api.post("sms_template", data=template) - print(f'Added `{template["name"]}`') - except APIError as e: - print(f"Couldn't add `{template['name']}` because {str(e)}") + copy_templates('sms_template') # Assumes all the sequence steps (templates) were already transferred over if args.sequences or args.all: From 4534789afa1f9155585650a6d558e09e82b3ac1d Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 7 Jun 2023 11:20:36 -0500 Subject: [PATCH 2/5] Add comments --- scripts/clone_organization.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/scripts/clone_organization.py b/scripts/clone_organization.py index 017fd0f..085d1eb 100755 --- a/scripts/clone_organization.py +++ b/scripts/clone_organization.py @@ -109,7 +109,7 @@ if confirmed not in ["yes", "y"]: exit() -# Get lead statuses, remove old ids, then post to new org +# Copy lead statuses if args.lead_statuses or args.statuses or args.all: print("\nCopying Lead Statuses") @@ -178,6 +178,7 @@ def copy_custom_fields(custom_field_type): try: if from_cf['is_shared']: + # check to see if shared custom field already exists to_cf = next( ( x @@ -186,7 +187,7 @@ def copy_custom_fields(custom_field_type): ), None, ) - + # add shared custom fields that do not already exist if not to_cf: to_cf = to_api.post(f"custom_field/shared", data=from_cf) print(f'Created `{from_cf["name"]}` shared custom field') @@ -210,19 +211,22 @@ def copy_custom_fields(custom_field_type): except APIError as e: print(f"Couldn't add `{from_cf['name']}` because {str(e)}") - +# copy lead custom fields if args.lead_custom_fields or args.custom_fields or args.all: print("\nCopying Lead Custom Fields") copy_custom_fields('lead') +# copy opportunity custom fields if args.opportunity_custom_fields or args.custom_fields or args.all: print("\nCopying Opportunity Custom Fields") copy_custom_fields('opportunity') +# copy contact custom fields if args.contact_custom_fields or args.custom_fields or args.all: print("\nCopying Contact Custom Fields") copy_custom_fields('contact') +# copy integration links if args.integration_links or args.all: print("\nCopying Integration Links") integration_links = from_api.get_all_items('integration_link') @@ -236,6 +240,7 @@ def copy_custom_fields(custom_field_type): except APIError as e: print(f"Couldn't add `{link['name']}` because {str(e)}") +# copy roles if args.roles or args.all: BUILT_IN_ROLES = [ "Admin", @@ -247,6 +252,7 @@ def copy_custom_fields(custom_field_type): print("\nCopying Roles") roles = from_api.get_all_items('role') for role in roles: + # skip built in roles if role["name"] in BUILT_IN_ROLES: continue @@ -272,10 +278,12 @@ def copy_templates(template_type): except APIError as e: print(f"Couldn't add `{template['name']}` because {str(e)}") +# Copy email templates if args.templates or args.email_templates or args.all: print("\nCopying Email Templates") copy_templates('email_template') +# copy sms templates if args.templates or args.sms_templates or args.all: print("\nCopying SMS Templates") copy_templates('sms_template') @@ -283,9 +291,10 @@ def copy_templates(template_type): # Assumes all the sequence steps (templates) were already transferred over if args.sequences or args.all: print("\nCopying Sequences") - + # get existing templates in new org to_email_templates = to_api.get_all_items('email_template') to_sms_templates = to_api.get_all_items('sms_template') + # get sequences in old org from_sequences = from_api.get_all_items('sequence') for sequence in from_sequences: del sequence["id"] @@ -325,6 +334,7 @@ def copy_templates(template_type): except APIError as e: print(f"Couldn't add `{sequence['name']}` because {str(e)}") +# Copy custom activities if args.custom_activities or args.all: print("\nCopying Custom Activities") @@ -378,6 +388,7 @@ def copy_templates(template_type): from_field.pop('organization_id', None) if field["is_shared"]: + # check if field already exists to_field = next( ( x @@ -419,6 +430,7 @@ def copy_templates(template_type): from_field["custom_activity_type_id"] = new_activity_type["id"] to_api.post("custom_field/activity/", data=from_field) +# copy groups if args.groups or args.groups_with_members or args.all: print("\nCopying Groups") groups = from_api.get('group')['data'] @@ -429,6 +441,7 @@ def copy_templates(template_type): new_group = to_api.post('group', data={'name': group['name']}) if args.groups_with_members: + # copy group members for member in group['members']: try: to_api.post(f'group/{new_group["id"]}/member', data={'user_id': member['user_id']}) @@ -440,6 +453,7 @@ def copy_templates(template_type): except APIError as e: print(f"Couldn't add `{group['name']}` because {str(e)}") +# copy smart views if args.smart_views or args.all: def structured_replace(value, replacement_dictionary): @@ -475,6 +489,7 @@ def textual_replace(value, replacement_dictionary): def get_id_mappings(): + # create mapping dictionary of all custom objects accessible by smart views map_from_to_id = {} # Custom Activity Types @@ -609,7 +624,7 @@ def get_smartviews(api): print("\nCopying Smart Views") from_smart_views = get_smartviews(from_api) - # Filter our Smart Views that already exist (by name) + # Filter out Smart Views that already exist (by name) to_smart_views = get_smartviews(to_api) to_smart_view_names = [x['name'] for x in to_smart_views] from_smart_views = [x for x in from_smart_views if x['name'] not in to_smart_view_names] @@ -630,7 +645,7 @@ def get_smartviews(api): def get_memberships(api, organization): resp = api.get(f"organization/{organization['id']}", params={"_fields": "memberships,inactive_memberships"}) return resp["memberships"] + resp["inactive_memberships"] - + # map user access from old org to new org from_memberships = get_memberships(from_api, from_organization) to_memberships = get_memberships(to_api, to_organization) from_to_membership_id = {} @@ -698,6 +713,7 @@ def get_memberships(api, organization): if smart_view['s_query'] != s_query or smart_view['query'] != query: to_api.put(f"saved_search/{smart_view['id']}", data=smart_view) +# copy webhooks if args.webhooks: print("\nCopying Webhooks") webhooks = from_api.get_all_items('webhook') From abc349d1732c97d4e18575ea98352fb5c55cb4bd Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 7 Jun 2023 12:52:34 -0500 Subject: [PATCH 3/5] final cleanup and test --- scripts/clone_organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/clone_organization.py b/scripts/clone_organization.py index 085d1eb..cec0ba2 100755 --- a/scripts/clone_organization.py +++ b/scripts/clone_organization.py @@ -2,7 +2,7 @@ from closeio_api import APIError -from scripts.CloseApiWrapper import CloseApiWrapper +from CloseApiWrapper import CloseApiWrapper arg_parser = argparse.ArgumentParser( description="Clone one organization to another" From c61c96d5f4a028ac77b15434d94bd34ec8821bd7 Mon Sep 17 00:00:00 2001 From: Sarah Date: Thu, 8 Jun 2023 11:25:25 -0500 Subject: [PATCH 4/5] add arg --clear-lead-and-status --- scripts/clone_organization.py | 49 ++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/scripts/clone_organization.py b/scripts/clone_organization.py index cec0ba2..f78f941 100755 --- a/scripts/clone_organization.py +++ b/scripts/clone_organization.py @@ -93,6 +93,11 @@ arg_parser.add_argument( "--all", "-a", action="store_true", help="Copy all settings" ) +arg_parser.add_argument( + "--clear-lead-and-status", + action="store_true", + help="Remove leads and lead statuses from destination org", +) args = arg_parser.parse_args() from_api = CloseApiWrapper(args.from_api_key) @@ -103,12 +108,46 @@ # ask user to confirm changes message = f"Cloning `{from_organization['name']}` ({from_organization['id']}) organization to `{to_organization['name']}` ({to_organization['id']})..." -message += '\nData from source organization will be added to the destination organization. No data will be deleted.\n\nContinue?' +if not args.clear_lead_and_status: + message += '\nData from source organization will be added to the destination organization. No data will be deleted.\n\nContinue?' +else: + message += '\nData from source organization will be added to the destination organization.\n\nContinue?' confirmed = input(f"{message} (y/n)\n") if confirmed not in ["yes", "y"]: exit() +# Clear destination leads and lead statuses +if args.clear_lead_and_status: + delete_message = 'Existing leads and lead statuses will be deleted from the destination organization. \n Continue?' + delete_confirmation = input(f"{delete_message} (y/n)") + if delete_confirmation in ["yes", "y"]: + print("\nDeleting existing leads in destination org") + to_leads = to_api.get_all_items('lead', params={'_fields': 'id,name'}) + for lead in to_leads: + try: + to_api.delete(f"lead/{lead['id']}/") + print(f"Removed lead {lead['name']}") + except APIError as e: + print(f"Couldn't remove lead `{lead['name']}` because {str(e)}") + + print("\nDeleting lead statuses in destination org") + to_lead_statuses = to_api.get_lead_statuses() + # at least one status must exist so we can rename the first one to have the label match the id for now + first_status_to = to_lead_statuses[0] + del to_lead_statuses[0] + first_status_to['label'] = first_status_to['id'] + try: + to_api.put(f"status/lead/{first_status_to['id']}", data=first_status_to) + except APIError as e: + print(f"Couldn't remove `{first_status_to['label']}` because {str(e)}") + for status in to_lead_statuses: + try: + to_api.delete(f"status/lead/{status['id']}") + print(f"Removed lead status {status['label']}") + except APIError as e: + print(f"Couldn't remove `{status['label']}` because {str(e)}") + # Copy lead statuses if args.lead_statuses or args.statuses or args.all: print("\nCopying Lead Statuses") @@ -122,6 +161,13 @@ print(f'Added lead status `{status["label"]}`') except APIError as e: print(f"Couldn't add `{status['label']}` because {str(e)}") + if first_status_to: + # if original statuses in destination org need to be removed, this removes the one remaining + try: + to_api.delete(f"status/lead/{first_status_to['id']}") + print(f"Removed old default lead status {first_status_to['label']}") + except APIError as e: + print(f"Couldn't remove `{first_status_to['label']}` because {str(e)}") # Copy pipelines and associated opportunity statuses if args.opportunity_statuses or args.statuses or args.all: @@ -725,3 +771,4 @@ def get_memberships(api, organization): print(f'Added `{webhook["url"]}`') except APIError as e: print(f"Couldn't add `{webhook['url']}` because {str(e)}") + From 3d507ba54136dd32282a2d0c293d639aad851e17 Mon Sep 17 00:00:00 2001 From: Sarah Date: Thu, 8 Jun 2023 11:31:53 -0500 Subject: [PATCH 5/5] fixed lead status update logic --- scripts/clone_organization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/clone_organization.py b/scripts/clone_organization.py index f78f941..7b62fc4 100755 --- a/scripts/clone_organization.py +++ b/scripts/clone_organization.py @@ -161,7 +161,7 @@ print(f'Added lead status `{status["label"]}`') except APIError as e: print(f"Couldn't add `{status['label']}` because {str(e)}") - if first_status_to: + if args.clear_lead_and_status and delete_confirmation in ["yes", "y"]: # if original statuses in destination org need to be removed, this removes the one remaining try: to_api.delete(f"status/lead/{first_status_to['id']}")