pyxecm 1.3.0__py3-none-any.whl → 1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyxecm might be problematic. Click here for more details.

@@ -0,0 +1,1782 @@
1
+ """
2
+ Salesforce Module to interact with the Salesforce API
3
+ See: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_rest.htm
4
+
5
+ Class: Salesforce
6
+ Methods:
7
+
8
+ __init__ : class initializer
9
+ config : Returns config data set
10
+ credentials: Returns the token data
11
+ request_header: Returns the request header for Salesforce API calls
12
+ parse_request_response: Parse the REST API responses and convert
13
+ them to Python dict in a safe way
14
+ exist_result_item: Check if an dict item is in the response
15
+ of the Salesforce API call
16
+ get_result_value: Check if a defined value (based on a key) is in the Salesforce API response
17
+
18
+ authenticate: Authenticates at Salesforce API
19
+
20
+ get_object_id_by_name: Get the ID of a given Salesforce object with a given type and name
21
+ get_object: Get a Salesforce object based on a defined
22
+ field value and return selected result fields.
23
+ add_object: Add object to Salesforce. This is a generic wrapper method
24
+ for the actual add methods.
25
+
26
+ get_group: Get a Salesforce group based on its ID.
27
+ add_group: Add a new Salesforce group.
28
+ update_group: Update a Salesforce group.
29
+ get_group_members: Get Salesforce group members
30
+ add_group_member: Add a user or group to a Salesforce group
31
+
32
+ get_all_user_profiles: Get all user profiles
33
+ get_user_profile_id: Get a user profile ID by profile name
34
+ get_user_id: Get a user ID by user name
35
+ get_user: Get a Salesforce user based on its ID.
36
+ add_user: Add a new Salesforce user.
37
+ update_user: Update a Salesforce user.
38
+ update_user_password: Update the password of a Salesforce user.
39
+ update_user_photo: update the Salesforce user photo.
40
+
41
+ add_account: Add a new Account object to Salesforce.
42
+ add_product: Add a new Product object to Salesforce.
43
+ add_opportunity: Add a new Opportunity object to Salesfoce.
44
+ add_case: Add a new Case object to Salesforce. The case number
45
+ is automatically created and can not be provided.
46
+ add_asset: Add a new Asset object to Salesforce.
47
+ add_contract: Add a new Contract object to Salesforce.
48
+ """
49
+
50
+ __author__ = "Dr. Marc Diefenbruch"
51
+ __copyright__ = "Copyright 2024, OpenText"
52
+ __credits__ = ["Kai-Philip Gatzweiler"]
53
+ __maintainer__ = "Dr. Marc Diefenbruch"
54
+ __email__ = "mdiefenb@opentext.com"
55
+
56
+ import os
57
+ import json
58
+ import logging
59
+
60
+ from typing import Optional, Union, Any
61
+ import requests
62
+
63
+ logger = logging.getLogger("pyxecm.customizer.salesforce")
64
+
65
+ REQUEST_LOGIN_HEADERS = {
66
+ "Content-Type": "application/x-www-form-urlencoded",
67
+ "Accept": "application/json",
68
+ }
69
+
70
+ REQUEST_TIMEOUT = 60
71
+ SALESFORCE_API_VERSION = "v60.0"
72
+
73
+ class Salesforce(object):
74
+ """Used to retrieve and automate stettings in Salesforce."""
75
+
76
+ _config: dict
77
+ _access_token = None
78
+ _instance_url = None
79
+
80
+ def __init__(
81
+ self,
82
+ base_url: str,
83
+ client_id: str,
84
+ client_secret: str,
85
+ username: str,
86
+ password: str,
87
+ authorization_url: str = "",
88
+ security_token: str = "",
89
+ ):
90
+ """Initialize the Salesforce object
91
+
92
+ Args:
93
+ base_url (str): base URL of the Salesforce tenant
94
+ authorization_url (str): authorization URL of the Salesforce tenant, typically ending with "/services/oauth2/token"
95
+ client_id (str): Salesforce Client ID
96
+ client_secret (str): Salesforce Client Secret
97
+ username (str): user name in Saleforce
98
+ password (str): password of the user
99
+ authorization_url (str, optional): URL for Salesforce login. If not given it will be constructed with default values
100
+ using base_url
101
+ security_token (str, optional): security token for Salesforce login
102
+ """
103
+
104
+ # The instance URL is also returned by the authenticate call
105
+ # but typically it is identical to the base_url.
106
+ self._instance_url = base_url
107
+
108
+ salesforce_config = {}
109
+
110
+ # Store the credentials and parameters in a config dictionary:
111
+ salesforce_config["clientId"] = client_id
112
+ salesforce_config["clientSecret"] = client_secret
113
+ salesforce_config["username"] = username
114
+ salesforce_config["password"] = password
115
+ salesforce_config["securityToken"] = security_token
116
+
117
+ # Set the Salesforce URLs and REST API endpoints:
118
+ salesforce_config["baseUrl"] = base_url
119
+ salesforce_config["objectUrl"] = salesforce_config[
120
+ "baseUrl"
121
+ ] + "/services/data/{}/sobjects/".format(SALESFORCE_API_VERSION)
122
+ salesforce_config["queryUrl"] = salesforce_config[
123
+ "baseUrl"
124
+ ] + "/services/data/{}/query/".format(SALESFORCE_API_VERSION)
125
+ salesforce_config["compositeUrl"] = salesforce_config[
126
+ "baseUrl"
127
+ ] + "/services/data/{}/composite/".format(SALESFORCE_API_VERSION)
128
+ salesforce_config["connectUrl"] = salesforce_config[
129
+ "baseUrl"
130
+ ] + "/services/data/{}/connect/".format(SALESFORCE_API_VERSION)
131
+ salesforce_config["toolingUrl"] = salesforce_config[
132
+ "baseUrl"
133
+ ] + "/services/data/{}/tooling/".format(SALESFORCE_API_VERSION)
134
+ if authorization_url:
135
+ salesforce_config["authenticationUrl"] = authorization_url
136
+ else:
137
+ salesforce_config["authenticationUrl"] = (
138
+ salesforce_config["baseUrl"] + "/services/oauth2/token"
139
+ )
140
+ # URLs that are based on the objectURL (sobjects/):
141
+ salesforce_config["userUrl"] = salesforce_config["objectUrl"] + "User/"
142
+ salesforce_config["groupUrl"] = salesforce_config["objectUrl"] + "Group/"
143
+ salesforce_config["groupMemberUrl"] = (
144
+ salesforce_config["objectUrl"] + "GroupMember/"
145
+ )
146
+ salesforce_config["accountUrl"] = salesforce_config["objectUrl"] + "Account/"
147
+ salesforce_config["productUrl"] = salesforce_config["objectUrl"] + "Product2/"
148
+ salesforce_config["opportunityUrl"] = (
149
+ salesforce_config["objectUrl"] + "Opportunity/"
150
+ )
151
+ salesforce_config["caseUrl"] = salesforce_config["objectUrl"] + "Case/"
152
+ salesforce_config["assetUrl"] = salesforce_config["objectUrl"] + "Asset/"
153
+ salesforce_config["contractUrl"] = salesforce_config["objectUrl"] + "Contract/"
154
+
155
+ # Set the data for the token request
156
+ salesforce_config["authenticationData"] = {
157
+ "grant_type": "password",
158
+ "client_id": client_id,
159
+ "client_secret": client_secret,
160
+ "username": username,
161
+ "password": password,
162
+ }
163
+
164
+ self._config = salesforce_config
165
+
166
+ # end method definition
167
+
168
+ def config(self) -> dict:
169
+ """Returns the configuration dictionary
170
+
171
+ Returns:
172
+ dict: Configuration dictionary
173
+ """
174
+ return self._config
175
+
176
+ # end method definition
177
+
178
+ def credentials(self) -> dict:
179
+ """Return the login credentials
180
+
181
+ Returns:
182
+ dict: dictionary with login credentials for Salesforce
183
+ """
184
+ return self.config()["authenticationData"]
185
+
186
+ # end method definition
187
+
188
+ def request_header(self, content_type: str = "application/json") -> dict:
189
+ """Returns the request header used for Application calls.
190
+ Consists of Bearer access token and Content Type
191
+
192
+ Args:
193
+ content_type (str, optional): content type for the request
194
+ Return:
195
+ dict: request header values
196
+ """
197
+
198
+ request_header = {
199
+ "Authorization": "Bearer {}".format(self._access_token),
200
+ }
201
+ if content_type:
202
+ request_header["Content-Type"] = content_type
203
+
204
+ return request_header
205
+
206
+ # end method definition
207
+
208
+ def parse_request_response(
209
+ self,
210
+ response_object: requests.Response,
211
+ additional_error_message: str = "",
212
+ show_error: bool = True,
213
+ ) -> dict | None:
214
+ """Converts the request response (JSon) to a Python dict in a safe way
215
+ that also handles exceptions. It first tries to load the response.text
216
+ via json.loads() that produces a dict output. Only if response.text is
217
+ not set or is empty it just converts the response_object to a dict using
218
+ the vars() built-in method.
219
+
220
+ Args:
221
+ response_object (object): this is reponse object delivered by the request call
222
+ additional_error_message (str, optional): use a more specific error message
223
+ in case of an error
224
+ show_error (bool): True: write an error to the log file
225
+ False: write a warning to the log file
226
+ Returns:
227
+ dict: response information or None in case of an error
228
+ """
229
+
230
+ if not response_object:
231
+ return None
232
+
233
+ try:
234
+ if response_object.text:
235
+ dict_object = json.loads(response_object.text)
236
+ else:
237
+ dict_object = vars(response_object)
238
+ except json.JSONDecodeError as exception:
239
+ if additional_error_message:
240
+ message = "Cannot decode response as JSon. {}; error -> {}".format(
241
+ additional_error_message, exception
242
+ )
243
+ else:
244
+ message = "Cannot decode response as JSon; error -> {}".format(
245
+ exception
246
+ )
247
+ if show_error:
248
+ logger.error(message)
249
+ else:
250
+ logger.warning(message)
251
+ return None
252
+ else:
253
+ return dict_object
254
+
255
+ # end method definition
256
+
257
+ def exist_result_item(self, response: dict, key: str, value: str) -> bool:
258
+ """Check existence of key / value pair in the response properties of an Salesforce API call.
259
+
260
+ Args:
261
+ response (dict): REST response from an Salesforce API call
262
+ key (str): property name (key)
263
+ value (str): value to find in the item with the matching key
264
+ Returns:
265
+ bool: True if the value was found, False otherwise
266
+ """
267
+
268
+ if not response:
269
+ return False
270
+
271
+ if "records" in response:
272
+ records = response["records"]
273
+ if not records or not isinstance(records, list):
274
+ return False
275
+
276
+ for record in records:
277
+ if value == record[key]:
278
+ return True
279
+ else:
280
+ if not key in response:
281
+ return False
282
+ if value == response[key]:
283
+ return True
284
+
285
+ return False
286
+
287
+ # end method definition
288
+
289
+ def get_result_value(
290
+ self,
291
+ response: dict,
292
+ key: str,
293
+ index: int = 0,
294
+ ) -> str | None:
295
+ """Get value of a result property with a given key of an Salesforce API call.
296
+
297
+ Args:
298
+ response (dict): REST response from an Salesforce REST Call
299
+ key (str): property name (key)
300
+ index (int, optional): Index to use (1st element has index 0).
301
+ Defaults to 0.
302
+ Returns:
303
+ str: value for the key, None otherwise
304
+ """
305
+
306
+ if not response:
307
+ return None
308
+
309
+ # do we have a complex response - e.g. from an SOQL query?
310
+ # these have list of "records":
311
+ if "records" in response:
312
+ values = response["records"]
313
+ if not values or not isinstance(values, list) or len(values) - 1 < index:
314
+ return None
315
+ value = values[index][key]
316
+ else: # simple response - try to find key in response directly:
317
+ if not key in response:
318
+ return None
319
+ value = response[key]
320
+
321
+ return value
322
+
323
+ # end method definition
324
+
325
+ def authenticate(self, revalidate: bool = False) -> str | None:
326
+ """Authenticate at Salesforce with client ID and client secret.
327
+
328
+ Args:
329
+ revalidate (bool, optional): determinse if a re-athentication is enforced
330
+ (e.g. if session has timed out with 401 error)
331
+ Returns:
332
+ str: Access token. Also stores access token in self._access_token. None in case of error
333
+ """
334
+
335
+ # Already authenticated and session still valid?
336
+ if self._access_token and not revalidate:
337
+ logger.debug(
338
+ "Session still valid - return existing access token -> %s",
339
+ str(self._access_token),
340
+ )
341
+ return self._access_token
342
+
343
+ request_url = self.config()["authenticationUrl"]
344
+ request_header = REQUEST_LOGIN_HEADERS
345
+
346
+ logger.debug("Requesting Salesforce Access Token from -> %s", request_url)
347
+
348
+ authenticate_post_body = self.credentials()
349
+
350
+ response = None
351
+ self._access_token = None
352
+ self._instance_url = None
353
+
354
+ try:
355
+ response = requests.post(
356
+ request_url,
357
+ data=authenticate_post_body,
358
+ headers=request_header,
359
+ timeout=REQUEST_TIMEOUT,
360
+ )
361
+ except requests.exceptions.ConnectionError as exception:
362
+ logger.warning(
363
+ "Unable to connect to -> %s : %s",
364
+ self.config()["authenticationUrl"],
365
+ exception,
366
+ )
367
+ return None
368
+
369
+ if response.ok:
370
+ authenticate_dict = self.parse_request_response(response)
371
+ if not authenticate_dict:
372
+ return None
373
+ else:
374
+ # Store authentication access_token:
375
+ self._access_token = authenticate_dict["access_token"]
376
+ logger.debug("Access Token -> %s", self._access_token)
377
+ self._instance_url = authenticate_dict["instance_url"]
378
+ logger.debug("Instance URL -> %s", self._instance_url)
379
+ else:
380
+ logger.error(
381
+ "Failed to request an Salesforce Access Token; error -> %s",
382
+ response.text,
383
+ )
384
+ return None
385
+
386
+ return self._access_token
387
+
388
+ # end method definition
389
+
390
+ def get_object_id_by_name(
391
+ self, object_type: str, name: str, name_field: str = "Name"
392
+ ) -> Optional[str]:
393
+ """Get the ID of a given Salesforce object with a given type and name.
394
+
395
+ Args:
396
+ object_type (str): Sales object type, like "Account", "Case", ...
397
+ name (str): Name of the Salesforce object.
398
+ name_field (str, optional): Field where the name is stored. Defaults to "Name".
399
+
400
+ Returns:
401
+ Optional[str]: Object ID or None if the request fails.
402
+ """
403
+
404
+ if not self._access_token or not self._instance_url:
405
+ self.authenticate()
406
+
407
+ request_header = self.request_header()
408
+ request_url = self.config()["queryUrl"]
409
+
410
+ query = f"SELECT Id FROM {object_type} WHERE {name_field} = '{name}'"
411
+
412
+ retries = 0
413
+ while True:
414
+ response = requests.get(
415
+ url=request_url,
416
+ headers=request_header,
417
+ params={"q": query},
418
+ timeout=REQUEST_TIMEOUT,
419
+ )
420
+ if response.ok:
421
+ response = self.parse_request_response(response)
422
+ object_id = self.get_result_value(response, "Id")
423
+ return object_id
424
+ elif response.status_code == 401 and retries == 0:
425
+ logger.debug("Session has expired - try to re-authenticate...")
426
+ self.authenticate(revalidate=True)
427
+ request_header = self.request_header()
428
+ retries += 1
429
+ else:
430
+ logger.error(
431
+ "Failed to get Salesforce object ID for object type -> '%s' and object name -> '%s'; status -> %s; error -> %s",
432
+ object_type,
433
+ name,
434
+ response.status_code,
435
+ response.text,
436
+ )
437
+ return None
438
+
439
+ # end method definition
440
+
441
+ def get_object(
442
+ self,
443
+ object_type: str,
444
+ search_field: str,
445
+ search_value: str,
446
+ result_fields: list | None,
447
+ limit: int = 200,
448
+ ) -> dict | None:
449
+ """Get a Salesforce object based on a defined field value and return selected result fields.
450
+
451
+ Args:
452
+ object_type (str): Salesforce Business Object type. Such as "Account" or "Case".
453
+ search_field (str): object field to search in
454
+ search_value (str): value to search for
455
+ result_fields (list | None): list of fields to return. If None, then all standard fields
456
+ of the object will be returned.
457
+ limit (int, optional): maximum number of fields to return. Salesforce enforces 200 as upper limit.
458
+
459
+ Returns:
460
+ dict | None: Dictionary with the Salesforce object data.
461
+
462
+ Example response:
463
+ {
464
+ 'totalSize': 2,
465
+ 'done': True,
466
+ 'records': [
467
+ {
468
+ 'attributes': {
469
+ 'type': 'Opportunity',
470
+ 'url': '/services/data/v60.0/sobjects/Opportunity/006Dn00000EclybIAB'
471
+ },
472
+ 'Id': '006Dn00000EclybIAB'
473
+ },
474
+ {
475
+ 'attributes': {
476
+ 'type': 'Opportunity',
477
+ 'url': '/services/data/v60.0/sobjects/Opportunity/006Dn00000EclyfIAB'
478
+ },
479
+ 'Id': '006Dn00000EclyfIAB'
480
+ }
481
+ ]
482
+ }
483
+ """
484
+
485
+ if not self._access_token or not self._instance_url:
486
+ self.authenticate()
487
+
488
+ if search_field and not search_value:
489
+ logger.error(
490
+ "No search value has been provided for search field -> %s!",
491
+ search_field,
492
+ )
493
+ return None
494
+ if not result_fields:
495
+ logger.debug(
496
+ "No result fields defined. Using 'FIELDS(STANDARD)' to deliver all standard fields of the object."
497
+ )
498
+ result_fields = ["FIELDS(STANDARD)"]
499
+
500
+ query = "SELECT {} FROM {}".format(", ".join(result_fields), object_type)
501
+ if search_field and search_value:
502
+ query += " WHERE {}='{}'".format(search_field, search_value)
503
+ query += " LIMIT {}".format(str(limit))
504
+
505
+ request_header = self.request_header()
506
+ request_url = self.config()["queryUrl"] + "?q={}".format(query)
507
+
508
+ logger.debug(
509
+ "Sending query -> %s to Salesforce; calling -> %s", query, request_url
510
+ )
511
+
512
+ retries = 0
513
+ while True:
514
+ response = requests.get(
515
+ request_url, headers=request_header, timeout=REQUEST_TIMEOUT
516
+ )
517
+ if response.ok:
518
+ return self.parse_request_response(response)
519
+ elif response.status_code == 401 and retries == 0:
520
+ logger.debug("Session has expired - try to re-authenticate...")
521
+ self.authenticate(revalidate=True)
522
+ request_header = self.request_header()
523
+ retries += 1
524
+ else:
525
+ logger.error(
526
+ "Failed to retrieve Salesforce object -> %s with %s = %s; status -> %s; error -> %s",
527
+ object_type,
528
+ search_field,
529
+ search_value,
530
+ response.status_code,
531
+ response.text,
532
+ )
533
+ return None
534
+
535
+ # end method definition
536
+
537
+ def add_object(self, object_type: str, **kwargs: Any) -> dict | None:
538
+ """Add object to Salesforce. This is a generic wrapper method
539
+ for the actual add methods.
540
+
541
+ Args:
542
+ object_type (str): Type of the Salesforce business object, like "Account" or "Case".
543
+ **kwargs (dict): keyword / value ictionary with additional parameters
544
+
545
+ Returns:
546
+ dict | None: Dictionary with the Salesforce object data or None if the request fails.
547
+ """
548
+
549
+ match object_type:
550
+ case "Account":
551
+ return self.add_account(
552
+ account_name=kwargs.pop("AccountName", None),
553
+ account_number=kwargs.pop("AccountNumber", None),
554
+ account_type=kwargs.pop("Type", None),
555
+ description=kwargs.pop("Description", None),
556
+ industry=kwargs.pop("Industry", None),
557
+ website=kwargs.pop("Website", None),
558
+ phone=kwargs.pop("Phone", None),
559
+ **kwargs,
560
+ )
561
+ case "Product":
562
+ return self.add_product(
563
+ product_name=kwargs.pop("Name", None),
564
+ product_code=kwargs.pop("ProductCode", None),
565
+ description=kwargs.pop("Description", None),
566
+ price=kwargs.pop("Price", None),
567
+ **kwargs,
568
+ )
569
+ case "Opportunity":
570
+ return self.add_opportunity(
571
+ name=kwargs.pop("Name", None),
572
+ stage=kwargs.pop("StageName", None),
573
+ close_date=kwargs.pop("CloseDate", None),
574
+ amount=kwargs.pop("Amount", None),
575
+ account_id=kwargs.pop("AccountId", None),
576
+ description=kwargs.pop("Description", None),
577
+ **kwargs,
578
+ )
579
+ case "Case":
580
+ return self.add_case(
581
+ subject=kwargs.pop("Subject", None),
582
+ description=kwargs.pop("Description", None),
583
+ status=kwargs.pop("Status", None),
584
+ priority=kwargs.pop("Priority", None),
585
+ origin=kwargs.pop("Origin", None),
586
+ account_id=kwargs.pop("AccountId", None),
587
+ owner_id=kwargs.pop("OwnerId", None),
588
+ asset_id=kwargs.pop("AssetId", None),
589
+ product_id=kwargs.pop("ProductId", None),
590
+ **kwargs,
591
+ )
592
+ case "Contract":
593
+ return self.add_contract(
594
+ account_id=kwargs.pop("AccountId", None),
595
+ start_date=kwargs.pop("ContractStartDate", None),
596
+ contract_term=kwargs.pop("ContractTerm", None),
597
+ status=kwargs.pop("Status", None),
598
+ description=kwargs.pop("Description", None),
599
+ contract_type=kwargs.pop("ContractType", None),
600
+ **kwargs,
601
+ )
602
+ case "Asset":
603
+ return self.add_asset(
604
+ asset_name=kwargs.pop("Name", None),
605
+ product_id=kwargs.pop("Product", None),
606
+ serial_number=kwargs.pop("SerialNumber", None),
607
+ status=kwargs.pop("Status", None),
608
+ purchase_date=kwargs.pop("PurchaseDate", None),
609
+ install_date=kwargs.pop("InstallDate", None),
610
+ description=kwargs.pop("AssetDescription", None),
611
+ **kwargs,
612
+ )
613
+ case _:
614
+ logger.error(
615
+ "Unsupported Salesforce business object -> %s!",
616
+ object_type,
617
+ )
618
+
619
+ # end method definition
620
+
621
+ def get_group_id(self, groupname: str) -> Optional[str]:
622
+ """Get a group ID by group name.
623
+
624
+ Args:
625
+ groupname (str): Name of the Group.
626
+
627
+ Returns:
628
+ Optional[str]: Technical ID of the group
629
+ """
630
+
631
+ return self.get_object_id_by_name(
632
+ object_type="Group", name=groupname, name_field="Name"
633
+ )
634
+
635
+ # end method definition
636
+
637
+ def get_group(self, group_id: str) -> dict | None:
638
+ """Get a Salesforce group based on its ID.
639
+
640
+ Args:
641
+ group_id (str): ID of the Salesforce group
642
+
643
+ Returns:
644
+ dict | None: Dictionary with the Salesforce group data or None if the request fails.
645
+ """
646
+
647
+ if not self._access_token or not self._instance_url:
648
+ self.authenticate()
649
+
650
+ request_header = self.request_header()
651
+ request_url = self.config()["groupUrl"] + group_id
652
+
653
+ logger.debug(
654
+ "Get Salesforce group with ID -> %s; calling -> %s", group_id, request_url
655
+ )
656
+
657
+ retries = 0
658
+ while True:
659
+ response = requests.get(
660
+ request_url, headers=request_header, timeout=REQUEST_TIMEOUT
661
+ )
662
+ if response.ok:
663
+ return self.parse_request_response(response)
664
+ elif response.status_code == 401 and retries == 0:
665
+ logger.debug("Session has expired - try to re-authenticate...")
666
+ self.authenticate(revalidate=True)
667
+ request_header = self.request_header()
668
+ retries += 1
669
+ else:
670
+ logger.error(
671
+ "Failed to get Salesforce group -> %s; status -> %s; error -> %s",
672
+ group_id,
673
+ response.status_code,
674
+ response.text,
675
+ )
676
+ return None
677
+
678
+ # end method definition
679
+
680
+ def add_group(
681
+ self,
682
+ group_name: str,
683
+ group_type: str = "Regular",
684
+ ) -> dict | None:
685
+ """Add a new Salesforce group.
686
+
687
+ Args:
688
+ group_name (str): Name of the new Salesforce group
689
+
690
+ Returns:
691
+ dict | None: Dictionary with the Salesforce Group data or None if the request fails.
692
+
693
+ Example response:
694
+ {
695
+ 'id': '00GDn000000KWE0MAO',
696
+ 'success': True,
697
+ 'errors': []
698
+ }
699
+ """
700
+
701
+ if not self._access_token or not self._instance_url:
702
+ self.authenticate()
703
+
704
+ request_header = self.request_header()
705
+ request_url = self.config()["groupUrl"]
706
+
707
+ payload = {"Name": group_name, "Type": group_type}
708
+
709
+ logger.debug(
710
+ "Adding Salesforce group -> %s; calling -> %s", group_name, request_url
711
+ )
712
+
713
+ retries = 0
714
+ while True:
715
+ response = requests.post(
716
+ request_url,
717
+ headers=request_header,
718
+ data=json.dumps(payload),
719
+ timeout=REQUEST_TIMEOUT,
720
+ )
721
+ if response.ok:
722
+ return self.parse_request_response(response)
723
+ elif response.status_code == 401 and retries == 0:
724
+ logger.debug("Session has expired - try to re-authenticate...")
725
+ self.authenticate(revalidate=True)
726
+ request_header = self.request_header()
727
+ retries += 1
728
+ else:
729
+ logger.error(
730
+ "Failed to add Salesforce group -> %s; status -> %s; error -> %s",
731
+ group_name,
732
+ response.status_code,
733
+ response.text,
734
+ )
735
+ return None
736
+
737
+ # end method definition
738
+
739
+ def update_group(
740
+ self,
741
+ group_id: str,
742
+ update_data: dict,
743
+ ) -> dict:
744
+ """Update a Salesforce group.
745
+
746
+ Args:
747
+ group_id (str): The Salesforce group ID.
748
+ update_data (dict): Dictionary containing the fields to update.
749
+
750
+ Returns:
751
+ dict: Response from the Salesforce API.
752
+ """
753
+
754
+ if not self._access_token or not self._instance_url:
755
+ self.authenticate()
756
+
757
+ request_header = self.request_header()
758
+
759
+ request_url = self.config()["groupUrl"] + group_id
760
+
761
+ logger.debug(
762
+ "Update Salesforce group with ID -> %s; calling -> %s",
763
+ group_id,
764
+ request_url,
765
+ )
766
+
767
+ retries = 0
768
+ while True:
769
+ response = requests.patch(
770
+ request_url,
771
+ json=update_data,
772
+ headers=request_header,
773
+ timeout=REQUEST_TIMEOUT,
774
+ )
775
+ if response.ok:
776
+ return self.parse_request_response(response)
777
+ elif response.status_code == 401 and retries == 0:
778
+ logger.debug("Session has expired - try to re-authenticate...")
779
+ self.authenticate(revalidate=True)
780
+ request_header = self.request_header()
781
+ retries += 1
782
+ else:
783
+ logger.error(
784
+ "Failed to update Salesforce group -> %s; status -> %s; error -> %s",
785
+ group_id,
786
+ response.status_code,
787
+ response.text,
788
+ )
789
+ return None
790
+
791
+ # end method definition
792
+
793
+ def get_group_members(self, group_id: str) -> list | None:
794
+ """Get Salesforce group members
795
+
796
+ Args:
797
+ group_id (str): Id of the group to retrieve the members
798
+
799
+ Returns:
800
+ list | None: result
801
+
802
+ Example response:
803
+ {
804
+ 'totalSize': 1,
805
+ 'done': True,
806
+ 'records': [
807
+ {
808
+ 'attributes': {
809
+ 'type': 'GroupMember',
810
+ 'url': '/services/data/v60.0/sobjects/GroupMember/011Dn000000ELhwIAG'
811
+ },
812
+ 'UserOrGroupId': '00GDn000000KWE5MAO'
813
+ }
814
+ ]
815
+ }
816
+ """
817
+
818
+ if not self._access_token or not self._instance_url:
819
+ self.authenticate()
820
+
821
+ request_header = self.request_header()
822
+
823
+ request_url = self.config()["queryUrl"]
824
+
825
+ query = f"SELECT UserOrGroupId FROM GroupMember WHERE GroupId = '{group_id}'"
826
+ params = {"q": query}
827
+
828
+ logger.debug(
829
+ "Get members of Salesforce group with ID -> %s; calling -> %s",
830
+ group_id,
831
+ request_url,
832
+ )
833
+
834
+ retries = 0
835
+ while True:
836
+ response = requests.get(
837
+ request_url,
838
+ headers=request_header,
839
+ params=params,
840
+ timeout=REQUEST_TIMEOUT,
841
+ )
842
+ if response.ok:
843
+ return self.parse_request_response(response)
844
+ elif response.status_code == 401 and retries == 0:
845
+ logger.debug("Session has expired - try to re-authenticate...")
846
+ self.authenticate(revalidate=True)
847
+ request_header = self.request_header()
848
+ retries += 1
849
+ else:
850
+ logger.error(
851
+ "Failed to retrieve members of Salesforce group with ID -> %s; status -> %s; error -> %s",
852
+ group_id,
853
+ response.status_code,
854
+ response.text,
855
+ )
856
+ return None
857
+
858
+ # end method definition
859
+
860
+ def add_group_member(self, group_id: str, member_id: str) -> dict | None:
861
+ """Add a user or group to a Salesforce group
862
+
863
+ Args:
864
+ group_id (str): ID of the Salesforce Group to add member to.
865
+ member_id (str): ID of the user or group.
866
+
867
+ Returns:
868
+ dict | None: Dictionary with the Salesforce membership data or None if the request fails.
869
+
870
+ Example response (id is the membership ID):
871
+ {
872
+ 'id': '011Dn000000ELhwIAG',
873
+ 'success': True,
874
+ 'errors': []
875
+ }
876
+ """
877
+
878
+ if not self._access_token or not self._instance_url:
879
+ self.authenticate()
880
+
881
+ request_url = self.config()["groupMemberUrl"]
882
+
883
+ request_header = self.request_header()
884
+
885
+ payload = {"GroupId": group_id, "UserOrGroupId": member_id}
886
+
887
+ logger.debug(
888
+ "Add member with ID -> %s to Salesforce group with ID -> %s; calling -> %s",
889
+ member_id,
890
+ group_id,
891
+ request_url,
892
+ )
893
+
894
+ retries = 0
895
+ while True:
896
+ response = requests.post(
897
+ request_url,
898
+ headers=request_header,
899
+ json=payload,
900
+ timeout=REQUEST_TIMEOUT,
901
+ )
902
+ if response.ok:
903
+ return self.parse_request_response(response)
904
+ elif response.status_code == 401 and retries == 0:
905
+ logger.debug("Session has expired - try to re-authenticate...")
906
+ self.authenticate(revalidate=True)
907
+ request_header = self.request_header()
908
+ retries += 1
909
+ else:
910
+ logger.error(
911
+ "Failed to retrieve members of Salesforce group with ID -> %s; status -> %s; error -> %s",
912
+ group_id,
913
+ response.status_code,
914
+ response.text,
915
+ )
916
+ return None
917
+
918
+ # end method definition
919
+
920
+ def get_all_user_profiles(self) -> dict | None:
921
+ """Get all user profiles
922
+
923
+ Returns:
924
+ dict | None: Dictionary with salesforce user profiles.
925
+
926
+ Example response:
927
+ {
928
+ 'totalSize': 15,
929
+ 'done': True,
930
+ 'records': [
931
+ {
932
+ ...
933
+ 'attributes': {
934
+ 'type': 'Profile',
935
+ 'url': '/services/data/v52.0/sobjects/Profile/00eDn000001msL8IAI'},
936
+ 'Id': '00eDn000001msL8IAI',
937
+ 'Name': 'Standard User',
938
+ 'CreatedById':
939
+ '005Dn000001rRodIAE',
940
+ 'CreatedDate': '2022-11-30T15:30:54.000+0000',
941
+ 'Description': None,
942
+ 'LastModifiedById': '005Dn000001rUacIAE',
943
+ 'LastModifiedDate': '2024-02-08T17:46:17.000+0000',
944
+ 'PermissionsCustomizeApplication': False,
945
+ 'PermissionsEditTask': True,
946
+ 'PermissionsImportLeads': False
947
+ }
948
+ }, ...
949
+ ]
950
+ }
951
+ """
952
+
953
+ if not self._access_token or not self._instance_url:
954
+ self.authenticate()
955
+
956
+ request_header = self.request_header()
957
+ request_url = self.config()["queryUrl"]
958
+
959
+ query = "SELECT Id, Name, CreatedById, CreatedDate, Description, LastModifiedById, LastModifiedDate, PermissionsCustomizeApplication, PermissionsEditTask, PermissionsImportLeads FROM Profile"
960
+
961
+ retries = 0
962
+ while True:
963
+ response = requests.get(
964
+ request_url,
965
+ headers=request_header,
966
+ params={"q": query},
967
+ timeout=REQUEST_TIMEOUT,
968
+ )
969
+ if response.ok:
970
+ return self.parse_request_response(response)
971
+ elif response.status_code == 401 and retries == 0:
972
+ logger.debug("Session has expired - try to re-authenticate...")
973
+ self.authenticate(revalidate=True)
974
+ request_header = self.request_header()
975
+ retries += 1
976
+ else:
977
+ logger.error(
978
+ "Failed to get Salesforce user profiles; status -> %s; error -> %s",
979
+ response.status_code,
980
+ response.text,
981
+ )
982
+ return None
983
+
984
+ # end method definition
985
+
986
+ def get_user_profile_id(self, profile_name: str) -> Optional[str]:
987
+ """Get a user profile ID by profile name.
988
+
989
+ Args:
990
+ profile_name (str): Name of the User Profile.
991
+
992
+ Returns:
993
+ Optional[str]: Technical ID of the user profile.
994
+ """
995
+
996
+ return self.get_object_id_by_name(object_type="Profile", name=profile_name)
997
+
998
+ # end method definition
999
+
1000
+ def get_user_id(self, username: str) -> Optional[str]:
1001
+ """Get a user ID by user name.
1002
+
1003
+ Args:
1004
+ username (str): Name of the User.
1005
+
1006
+ Returns:
1007
+ Optional[str]: Technical ID of the user
1008
+ """
1009
+
1010
+ return self.get_object_id_by_name(
1011
+ object_type="User", name=username, name_field="Username"
1012
+ )
1013
+
1014
+ # end method definition
1015
+
1016
+ def get_user(self, user_id: str) -> dict | None:
1017
+ """Get a Salesforce user based on its ID.
1018
+
1019
+ Args:
1020
+ user_id (str): ID of the Salesforce user
1021
+
1022
+ Returns:
1023
+ dict | None: Dictionary with the Salesforce user data or None if the request fails.
1024
+ """
1025
+
1026
+ if not self._access_token or not self._instance_url:
1027
+ self.authenticate()
1028
+
1029
+ request_header = self.request_header()
1030
+ request_url = self.config()["userUrl"] + user_id
1031
+
1032
+ logger.debug(
1033
+ "Get Salesforce user with ID -> %s; calling -> %s", user_id, request_url
1034
+ )
1035
+
1036
+ retries = 0
1037
+ while True:
1038
+ response = requests.get(
1039
+ request_url, headers=request_header, timeout=REQUEST_TIMEOUT
1040
+ )
1041
+ if response.ok:
1042
+ return self.parse_request_response(response)
1043
+ elif response.status_code == 401 and retries == 0:
1044
+ logger.debug("Session has expired - try to re-authenticate...")
1045
+ self.authenticate(revalidate=True)
1046
+ request_header = self.request_header()
1047
+ retries += 1
1048
+ else:
1049
+ logger.error(
1050
+ "Failed to get Salesforce user -> %s; status -> %s; error -> %s",
1051
+ user_id,
1052
+ response.status_code,
1053
+ response.text,
1054
+ )
1055
+ return None
1056
+
1057
+ # end method definition
1058
+
1059
+ def add_user(
1060
+ self,
1061
+ username: str,
1062
+ email: str,
1063
+ firstname: str,
1064
+ lastname: str,
1065
+ title: str | None = None,
1066
+ department: str | None = None,
1067
+ company_name: str = "Innovate",
1068
+ profile_name: Optional[str] = "Standard User",
1069
+ profile_id: Optional[str] = None,
1070
+ time_zone_key: Optional[str] = "America/Los_Angeles",
1071
+ email_encoding_key: Optional[str] = "ISO-8859-1",
1072
+ locale_key: Optional[str] = "en_US",
1073
+ alias: Optional[str] = None,
1074
+ ) -> dict | None:
1075
+ """Add a new Salesforce user. The password has to be set separately.
1076
+
1077
+ Args:
1078
+ username (str): Login name of the new user
1079
+ email (str): Email of the new user
1080
+ firstname (str): First name of the new user.
1081
+ lastname (str): Last name of the new user.
1082
+ title (str): Title of the user.
1083
+ department (str): Department of the user.
1084
+ company_name (str): Name of the Company of the user.
1085
+ profile_name (str): Profile name like "Standard User"
1086
+ profile_id (str, optional): Profile ID of the new user. Defaults to None.
1087
+ Use method get_all_user_profiles() to determine
1088
+ the desired Profile for the user. Or pass the profile_name.
1089
+ time_zone_key (str, optional) in format country/city like "America/Los_Angeles",
1090
+ email_encoding_key (str, optional). Default is "ISO-8859-1".
1091
+ locale_key (str, optional). Default is "en_US".
1092
+ alias (str, optional): Alias of the new user. Defaults to None.
1093
+
1094
+ Returns:
1095
+ dict | None: Dictionary with the Salesforce User data or None if the request fails.
1096
+ """
1097
+
1098
+ if not self._access_token or not self._instance_url:
1099
+ self.authenticate()
1100
+
1101
+ request_header = self.request_header()
1102
+ request_url = self.config()["userUrl"]
1103
+
1104
+ # if just a profile name is given then we determine the profile ID by the name:
1105
+ if profile_name and not profile_id:
1106
+ profile_id = self.get_user_profile_id(profile_name)
1107
+
1108
+ payload = {
1109
+ "Username": username,
1110
+ "Email": email,
1111
+ "FirstName": firstname,
1112
+ "LastName": lastname,
1113
+ "ProfileId": profile_id,
1114
+ "Department": department,
1115
+ "CompanyName": company_name,
1116
+ "Title": title,
1117
+ "Alias": alias if alias else username,
1118
+ "TimeZoneSidKey": time_zone_key, # Set default TimeZoneSidKey
1119
+ "LocaleSidKey": locale_key, # Set default LocaleSidKey
1120
+ "EmailEncodingKey": email_encoding_key, # Set default EmailEncodingKey
1121
+ "LanguageLocaleKey": locale_key, # Set default LanguageLocaleKey
1122
+ }
1123
+
1124
+ logger.debug(
1125
+ "Adding Salesforce user -> %s; calling -> %s", username, request_url
1126
+ )
1127
+
1128
+ retries = 0
1129
+ while True:
1130
+ response = requests.post(
1131
+ request_url,
1132
+ headers=request_header,
1133
+ data=json.dumps(payload),
1134
+ timeout=REQUEST_TIMEOUT,
1135
+ )
1136
+ if response.ok:
1137
+ return self.parse_request_response(response)
1138
+ elif response.status_code == 401 and retries == 0:
1139
+ logger.debug("Session has expired - try to re-authenticate...")
1140
+ self.authenticate(revalidate=True)
1141
+ request_header = self.request_header()
1142
+ retries += 1
1143
+ else:
1144
+ logger.error(
1145
+ "Failed to add Salesforce user -> %s; status -> %s; error -> %s",
1146
+ username,
1147
+ response.status_code,
1148
+ response.text,
1149
+ )
1150
+ return None
1151
+
1152
+ # end method definition
1153
+
1154
+ def update_user(
1155
+ self,
1156
+ user_id: str,
1157
+ update_data: dict,
1158
+ ) -> dict:
1159
+ """Update a Salesforce user.
1160
+
1161
+ Args:
1162
+ user_id (str): The Salesforce user ID.
1163
+ update_data (dict): Dictionary containing the fields to update.
1164
+
1165
+ Returns:
1166
+ dict: Response from the Salesforce API.
1167
+ """
1168
+
1169
+ if not self._access_token or not self._instance_url:
1170
+ self.authenticate()
1171
+
1172
+ request_header = self.request_header()
1173
+
1174
+ request_url = self.config()["userUrl"] + user_id
1175
+
1176
+ logger.debug(
1177
+ "Update Salesforce user with ID -> %s; calling -> %s", user_id, request_url
1178
+ )
1179
+
1180
+ retries = 0
1181
+ while True:
1182
+ response = requests.patch(
1183
+ request_url,
1184
+ json=update_data,
1185
+ headers=request_header,
1186
+ timeout=REQUEST_TIMEOUT,
1187
+ )
1188
+ if response.ok:
1189
+ return self.parse_request_response(response)
1190
+ elif response.status_code == 401 and retries == 0:
1191
+ logger.debug("Session has expired - try to re-authenticate...")
1192
+ self.authenticate(revalidate=True)
1193
+ request_header = self.request_header()
1194
+ retries += 1
1195
+ else:
1196
+ logger.error(
1197
+ "Failed to update Salesforce user -> %s; status -> %s; error -> %s",
1198
+ user_id,
1199
+ response.status_code,
1200
+ response.text,
1201
+ )
1202
+ return None
1203
+
1204
+ # end method definition
1205
+
1206
+ def update_user_password(
1207
+ self,
1208
+ user_id: str,
1209
+ password: str,
1210
+ ) -> dict:
1211
+ """Update the password of a Salesforce user.
1212
+
1213
+ Args:
1214
+ user_id (str): The Salesforce user ID.
1215
+ password (str): New user password.
1216
+
1217
+ Returns:
1218
+ dict: Response from the Salesforce API.
1219
+ """
1220
+
1221
+ if not self._access_token or not self._instance_url:
1222
+ self.authenticate()
1223
+
1224
+ request_header = self.request_header()
1225
+
1226
+ request_url = self.config()["userUrl"] + "{}/password".format(user_id)
1227
+
1228
+ logger.debug(
1229
+ "Update password of Salesforce user with ID -> %s; calling -> %s",
1230
+ user_id,
1231
+ request_url,
1232
+ )
1233
+
1234
+ update_data = {"NewPassword": password}
1235
+
1236
+ retries = 0
1237
+ while True:
1238
+ response = requests.post(
1239
+ request_url,
1240
+ json=update_data,
1241
+ headers=request_header,
1242
+ timeout=REQUEST_TIMEOUT,
1243
+ )
1244
+ if response.ok:
1245
+ return self.parse_request_response(response)
1246
+ elif response.status_code == 401 and retries == 0:
1247
+ logger.debug("Session has expired - try to re-authenticate...")
1248
+ self.authenticate(revalidate=True)
1249
+ request_header = self.request_header()
1250
+ retries += 1
1251
+ else:
1252
+ logger.error(
1253
+ "Failed to update password of Salesforce user -> %s; status -> %s; error -> %s",
1254
+ user_id,
1255
+ response.status_code,
1256
+ response.text,
1257
+ )
1258
+ return None
1259
+
1260
+ # end method definition
1261
+
1262
+ def update_user_photo(
1263
+ self,
1264
+ user_id: str,
1265
+ photo_path: str,
1266
+ ) -> dict | None:
1267
+ """Update the Salesforce user photo.
1268
+
1269
+ Args:
1270
+ user_id (str): Salesforce ID of the user
1271
+ photo_path (str): file system path with the location of the photo
1272
+ Returns:
1273
+ dict | None: Dictionary with the Salesforce User data or None if the request fails.
1274
+ """
1275
+
1276
+ if not self._access_token or not self._instance_url:
1277
+ self.authenticate()
1278
+
1279
+ # Check if the photo file exists
1280
+ if not os.path.isfile(photo_path):
1281
+ logger.error("Photo file -> %s not found!", photo_path)
1282
+ return None
1283
+
1284
+ try:
1285
+ # Read the photo file as binary data
1286
+ with open(photo_path, "rb") as image_file:
1287
+ photo_data = image_file.read()
1288
+ except OSError as exception:
1289
+ # Handle any errors that occurred while reading the photo file
1290
+ logger.error(
1291
+ "Error reading photo file -> %s; error -> %s", photo_path, exception
1292
+ )
1293
+ return None
1294
+
1295
+ request_header = self.request_header(content_type=None)
1296
+
1297
+ data = {"json": json.dumps({"cropX": 0, "cropY": 0, "cropSize": 200})}
1298
+ request_url = self.config()["connectUrl"] + f"user-profiles/{user_id}/photo"
1299
+ files = {
1300
+ "fileUpload": (
1301
+ photo_path,
1302
+ photo_data,
1303
+ "application/octet-stream",
1304
+ )
1305
+ }
1306
+
1307
+ logger.debug(
1308
+ "Update profile photo of Salesforce user with ID -> %s; calling -> %s",
1309
+ user_id,
1310
+ request_url,
1311
+ )
1312
+
1313
+ retries = 0
1314
+ while True:
1315
+ response = requests.post(
1316
+ request_url,
1317
+ files=files,
1318
+ data=data,
1319
+ headers=request_header,
1320
+ verify=False,
1321
+ timeout=REQUEST_TIMEOUT,
1322
+ )
1323
+ if response.ok:
1324
+ return self.parse_request_response(response)
1325
+ elif response.status_code == 401 and retries == 0:
1326
+ logger.debug("Session has expired - try to re-authenticate...")
1327
+ self.authenticate(revalidate=True)
1328
+ request_header = self.request_header()
1329
+ retries += 1
1330
+ else:
1331
+ logger.error(
1332
+ "Failed to update profile photo of Salesforce user with ID -> %s; status -> %s; error -> %s",
1333
+ user_id,
1334
+ response.status_code,
1335
+ response.text,
1336
+ )
1337
+ return None
1338
+
1339
+ # end method definition
1340
+
1341
+ def add_account(
1342
+ self,
1343
+ account_name: str,
1344
+ account_number: str,
1345
+ account_type: str = "Customer",
1346
+ description: Optional[str] = None,
1347
+ industry: Optional[str] = None,
1348
+ website: Optional[str] = None,
1349
+ phone: Optional[str] = None,
1350
+ **kwargs: Any,
1351
+ ) -> dict | None:
1352
+ """Add a new Account object to Salesforce.
1353
+
1354
+ Args:
1355
+ account_name (str): Name of the new Salesforce account.
1356
+ account_number (str): Number of the new Salesforce account (this is a logical number, not the technical ID)
1357
+ account_type (str): Type of the Salesforce account. Typical values are "Customer" or "Prospect".
1358
+ description(str, optional): Description of the new Salesforce account.
1359
+ industry (str, optional): Industry of the new Salesforce account. Defaults to None.
1360
+ website (str, optional): Website of the new Salesforce account. Defaults to None.
1361
+ phone (str, optional): Phone number of the new Salesforce account. Defaults to None.
1362
+ kwargs (Any): Additional values (e.g. custom fields)
1363
+
1364
+ Returns:
1365
+ dict | None: Dictionary with the Salesforce Account data or None if the request fails.
1366
+ """
1367
+
1368
+ if not self._access_token or not self._instance_url:
1369
+ self.authenticate()
1370
+
1371
+ request_header = self.request_header()
1372
+ request_url = self.config()["accountUrl"]
1373
+
1374
+ payload = {
1375
+ "Name": account_name,
1376
+ "AccountNumber": account_number,
1377
+ "Type": account_type,
1378
+ "Industry": industry,
1379
+ "Description": description,
1380
+ "Website": website,
1381
+ "Phone": phone,
1382
+ }
1383
+ payload.update(kwargs) # Add additional fields from kwargs
1384
+
1385
+ logger.debug(
1386
+ "Adding Salesforce account -> %s; calling -> %s", account_name, request_url
1387
+ )
1388
+
1389
+ retries = 0
1390
+ while True:
1391
+ response = requests.post(
1392
+ request_url,
1393
+ headers=request_header,
1394
+ data=json.dumps(payload),
1395
+ timeout=REQUEST_TIMEOUT,
1396
+ )
1397
+ if response.ok:
1398
+ return self.parse_request_response(response)
1399
+ elif response.status_code == 401 and retries == 0:
1400
+ logger.debug("Session has expired - try to re-authenticate...")
1401
+ self.authenticate(revalidate=True)
1402
+ request_header = self.request_header()
1403
+ retries += 1
1404
+ else:
1405
+ logger.error(
1406
+ "Failed to add Salesforce account -> %s; status -> %s; error -> %s",
1407
+ account_name,
1408
+ response.status_code,
1409
+ response.text,
1410
+ )
1411
+ return None
1412
+
1413
+ # end method definition
1414
+
1415
+ def add_product(
1416
+ self,
1417
+ product_name: str,
1418
+ product_code: str,
1419
+ description: str,
1420
+ price: float,
1421
+ **kwargs: Any,
1422
+ ) -> dict | None:
1423
+ """Add a new Product object to Salesforce.
1424
+
1425
+ Args:
1426
+ product_name (str): Name of the Salesforce Product.
1427
+ product_code (str): Code of the Salesforce Product.
1428
+ description (str): Description of the Salesforce Product.
1429
+ price (float): Price of the Salesforce Product.
1430
+
1431
+ Returns:
1432
+ dict | None: Dictionary with the Salesforce Product data or None if the request fails.
1433
+ """
1434
+
1435
+ if not self._access_token or not self._instance_url:
1436
+ self.authenticate()
1437
+
1438
+ request_header = self.request_header()
1439
+ request_url = self.config()["productUrl"]
1440
+
1441
+ payload = {
1442
+ "Name": product_name,
1443
+ "ProductCode": product_code,
1444
+ "Description": description,
1445
+ "Price__c": price,
1446
+ }
1447
+ payload.update(kwargs) # Add additional fields from kwargs
1448
+
1449
+ logger.debug(
1450
+ "Add Salesforce product -> %s; calling -> %s", product_name, request_url
1451
+ )
1452
+
1453
+ retries = 0
1454
+ while True:
1455
+ response = requests.post(
1456
+ request_url,
1457
+ headers=request_header,
1458
+ data=json.dumps(payload),
1459
+ timeout=REQUEST_TIMEOUT,
1460
+ )
1461
+ if response.ok:
1462
+ return self.parse_request_response(response)
1463
+ elif response.status_code == 401 and retries == 0:
1464
+ logger.debug("Session has expired - try to re-authenticate...")
1465
+ self.authenticate(revalidate=True)
1466
+ request_header = self.request_header()
1467
+ retries += 1
1468
+ else:
1469
+ logger.error(
1470
+ "Failed to add Salesforce product -> %s; status -> %s; error -> %s",
1471
+ product_name,
1472
+ response.status_code,
1473
+ response.text,
1474
+ )
1475
+ return None
1476
+
1477
+ # end method definition
1478
+
1479
+ def add_opportunity(
1480
+ self,
1481
+ name: str,
1482
+ stage: str,
1483
+ close_date: str,
1484
+ amount: Union[int, float],
1485
+ account_id: str,
1486
+ description: str = None,
1487
+ **kwargs: Any,
1488
+ ) -> dict | None:
1489
+ """Add a new Opportunity object to Salesfoce.
1490
+
1491
+ Args:
1492
+ name (str): Name of the Opportunity.
1493
+ stage (str): Stage of the Opportunity. Typical Value:
1494
+ "Prospecting", "Qualification", "Value Proposition", "Negotiation/Review",
1495
+ "Closed Won", "Closed Lost"
1496
+ close_date (str): Close date of the Opportunity. Should be in format YYYY-MM-DD.
1497
+ amount (Union[int, float]): Amount (expected revenue) of the opportunity.
1498
+ Can either be an integer or a float value.
1499
+ account_id (str): Technical ID of the related Salesforce Account.
1500
+
1501
+ Returns:
1502
+ dict | None: Dictionary with the Salesforce Opportunity data or None if the request fails.
1503
+ """
1504
+
1505
+ if not self._access_token or not self._instance_url:
1506
+ self.authenticate()
1507
+
1508
+ request_header = self.request_header()
1509
+ request_url = self.config()["opportunityUrl"]
1510
+
1511
+ payload = {
1512
+ "Name": name,
1513
+ "StageName": stage,
1514
+ "CloseDate": close_date,
1515
+ "Amount": amount,
1516
+ "AccountId": account_id,
1517
+ }
1518
+ if description:
1519
+ payload["Description"] = description
1520
+ payload.update(kwargs) # Add additional fields from kwargs
1521
+
1522
+ logger.debug(
1523
+ "Add Salesforce opportunity -> %s; calling -> %s", name, request_url
1524
+ )
1525
+
1526
+ retries = 0
1527
+ while True:
1528
+ response = requests.post(
1529
+ request_url,
1530
+ headers=request_header,
1531
+ data=json.dumps(payload),
1532
+ timeout=REQUEST_TIMEOUT,
1533
+ )
1534
+ if response.ok:
1535
+ return self.parse_request_response(response)
1536
+ elif response.status_code == 401 and retries == 0:
1537
+ logger.debug("Session has expired - try to re-authenticate...")
1538
+ self.authenticate(revalidate=True)
1539
+ request_header = self.request_header()
1540
+ retries += 1
1541
+ else:
1542
+ logger.error(
1543
+ "Failed to add Salesforce opportunity -> %s; status -> %s; error -> %s",
1544
+ name,
1545
+ response.status_code,
1546
+ response.text,
1547
+ )
1548
+ return None
1549
+
1550
+ # end method definition
1551
+
1552
+ def add_case(
1553
+ self,
1554
+ subject: str,
1555
+ description: str,
1556
+ status: str,
1557
+ priority: str,
1558
+ origin: str,
1559
+ account_id: str,
1560
+ owner_id: str,
1561
+ asset_id: Optional[str] = None,
1562
+ product_id: Optional[str] = None,
1563
+ **kwargs: Any,
1564
+ ) -> dict | None:
1565
+ """Add a new Case object to Salesforce. The case number is automatically created and can not be
1566
+ provided.
1567
+
1568
+ Args:
1569
+ subject (str): Subject (title) of the case. It's like the name.
1570
+ description (str): Description of the case
1571
+ status (str): Status of the case. Typecal values: "New", "On Hold", "Escalated"
1572
+ priority (str): Priority of the case. Typical values: "High", "Medium", "Low".
1573
+ origin (str): origin (source) of the case. Typical values: "Email", "Phone", "Web"
1574
+ account_id (str): technical ID of the related Account
1575
+ owner_id (str): owner of the case
1576
+ asset_id (str): technical ID of the related Asset
1577
+ product_id (str): technical ID of the related Product
1578
+ kwargs (Any): additional values (e.g. custom fields)
1579
+
1580
+ Returns:
1581
+ dict | None: Dictionary with the Salesforce Case data or None if the request fails.
1582
+ """
1583
+
1584
+ if not self._access_token or not self._instance_url:
1585
+ self.authenticate()
1586
+
1587
+ request_header = self.request_header()
1588
+ request_url = self.config()["caseUrl"]
1589
+
1590
+ payload = {
1591
+ "Subject": subject,
1592
+ "Description": description,
1593
+ "Status": status,
1594
+ "Priority": priority,
1595
+ "Origin": origin,
1596
+ "AccountId": account_id,
1597
+ "OwnerId": owner_id,
1598
+ }
1599
+
1600
+ if asset_id:
1601
+ payload["AssetId"] = asset_id
1602
+ if product_id:
1603
+ payload["ProductId"] = product_id
1604
+ payload.update(kwargs) # Add additional fields from kwargs
1605
+
1606
+ logger.debug("Add Salesforce case -> %s; calling -> %s", subject, request_url)
1607
+
1608
+ retries = 0
1609
+ while True:
1610
+ response = requests.post(
1611
+ request_url,
1612
+ headers=request_header,
1613
+ data=json.dumps(payload),
1614
+ timeout=REQUEST_TIMEOUT,
1615
+ )
1616
+ if response.ok:
1617
+ return self.parse_request_response(response)
1618
+ elif response.status_code == 401 and retries == 0:
1619
+ logger.debug("Session has expired - try to re-authenticate...")
1620
+ self.authenticate(revalidate=True)
1621
+ request_header = self.request_header()
1622
+ retries += 1
1623
+ else:
1624
+ logger.error(
1625
+ "Failed to add Salesforce case -> %s; status -> %s; error -> %s",
1626
+ subject,
1627
+ response.status_code,
1628
+ response.text,
1629
+ )
1630
+ return None
1631
+
1632
+ # end method definition
1633
+
1634
+ def add_asset(
1635
+ self,
1636
+ asset_name: str,
1637
+ product_id: str,
1638
+ serial_number: str,
1639
+ status: str,
1640
+ purchase_date: str,
1641
+ install_date: str,
1642
+ description: str | None = None,
1643
+ **kwargs: Any,
1644
+ ) -> dict | None:
1645
+ """Add a new Asset object to Salesforce.
1646
+
1647
+ Args:
1648
+ asset_name (str): Name of the Asset.
1649
+ product_id (str): Related Product ID.
1650
+ serial_number (str): Serial Number of the Asset.
1651
+ status (str): Status of the Asset. Typical values are "Purchased", "Shipped", "Installed", "Registered", "Obsolete"
1652
+ purchase_date (str): Purchase date of the Asset.
1653
+ install_date (str): Install date of the Asset.
1654
+ description (str): Description of the Asset.
1655
+ kwargs (Any): Additional values (e.g. custom fields)
1656
+
1657
+ Returns:
1658
+ dict | None: Dictionary with the Salesforce Asset data or None if the request fails.
1659
+ """
1660
+
1661
+ if not self._access_token or not self._instance_url:
1662
+ self.authenticate()
1663
+
1664
+ request_header = self.request_header()
1665
+ request_url = self.config()["assetUrl"]
1666
+
1667
+ payload = {
1668
+ "Name": asset_name,
1669
+ "ProductId": product_id,
1670
+ "SerialNumber": serial_number,
1671
+ "Status": status,
1672
+ "PurchaseDate": purchase_date,
1673
+ "InstallDate": install_date,
1674
+ }
1675
+ if description:
1676
+ payload["Description"] = description
1677
+ payload.update(kwargs) # Add additional fields from kwargs
1678
+
1679
+ logger.debug(
1680
+ "Add Salesforce asset -> %s; calling -> %s", asset_name, request_url
1681
+ )
1682
+
1683
+ retries = 0
1684
+ while True:
1685
+ response = requests.post(
1686
+ request_url,
1687
+ headers=request_header,
1688
+ data=json.dumps(payload),
1689
+ timeout=REQUEST_TIMEOUT,
1690
+ )
1691
+ if response.ok:
1692
+ return self.parse_request_response(response)
1693
+ elif response.status_code == 401 and retries == 0:
1694
+ logger.debug("Session has expired - try to re-authenticate...")
1695
+ self.authenticate(revalidate=True)
1696
+ request_header = self.request_header()
1697
+ retries += 1
1698
+ else:
1699
+ logger.error(
1700
+ "Failed to add Salesforce user -> %s; status -> %s; error -> %s",
1701
+ asset_name,
1702
+ response.status_code,
1703
+ response.text,
1704
+ )
1705
+ return None
1706
+
1707
+ # end method definition
1708
+
1709
+ def add_contract(
1710
+ self,
1711
+ account_id: str,
1712
+ start_date: str,
1713
+ contract_term: int,
1714
+ status: str = "Draft",
1715
+ description: Optional[str] = None,
1716
+ contract_type: Optional[str] = None,
1717
+ **kwargs: Any,
1718
+ ) -> dict | None:
1719
+ """Add a new Contract object to Salesforce.
1720
+
1721
+ Args:
1722
+ account_id (str): Technical ID of the related Salesforce Account object.
1723
+ start_date (str): Start date of the Contract. Use YYYY-MM-DD notation.
1724
+ contract_term (int): Term of the Contract in number of months, e.g. 48 for 4 years term.
1725
+ The end date of the contract will be calculated from start date + term.
1726
+ contract_type (str): Type of the Contract. Typical values are "Subscription",
1727
+ "Maintenance", "Support", "Lease", or "Service".
1728
+ status (str): Status of the Contract. Typical values are "Draft", "Activated", or "In Approval Process"
1729
+
1730
+ Returns:
1731
+ dict | None: Dictionary with the Salesforce user data or None if the request fails.
1732
+ """
1733
+
1734
+ if not self._access_token or not self._instance_url:
1735
+ self.authenticate()
1736
+
1737
+ request_header = self.request_header()
1738
+ request_url = self.config()["contractUrl"]
1739
+
1740
+ payload = {
1741
+ "AccountId": account_id,
1742
+ "StartDate": start_date,
1743
+ "ContractTerm": contract_term,
1744
+ "Status": status,
1745
+ }
1746
+ if description:
1747
+ payload["Description"] = description
1748
+ if contract_type:
1749
+ payload["ContractType"] = contract_type
1750
+ payload.update(kwargs) # Add additional fields from kwargs
1751
+
1752
+ logger.debug(
1753
+ "Adding Salesforce contract for account ID -> %s; calling -> %s",
1754
+ account_id,
1755
+ request_url,
1756
+ )
1757
+
1758
+ retries = 0
1759
+ while True:
1760
+ response = requests.post(
1761
+ request_url,
1762
+ headers=request_header,
1763
+ data=json.dumps(payload),
1764
+ timeout=REQUEST_TIMEOUT,
1765
+ )
1766
+ if response.ok:
1767
+ return self.parse_request_response(response)
1768
+ elif response.status_code == 401 and retries == 0:
1769
+ logger.debug("Session has expired - try to re-authenticate...")
1770
+ self.authenticate(revalidate=True)
1771
+ request_header = self.request_header()
1772
+ retries += 1
1773
+ else:
1774
+ logger.error(
1775
+ "Failed to add Salesforce contract for account ID -> %s; status -> %s; error -> %s",
1776
+ account_id,
1777
+ response.status_code,
1778
+ response.text,
1779
+ )
1780
+ return None
1781
+
1782
+ # end method definition