pyxecm 3.8.0__tar.gz → 3.8.2__tar.gz

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.
Files changed (76) hide show
  1. {pyxecm-3.8.0 → pyxecm-3.8.2}/PKG-INFO +1 -1
  2. {pyxecm-3.8.0 → pyxecm-3.8.2}/pyproject.toml +1 -1
  3. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/customizer.py +1 -0
  4. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/guidewire.py +21 -2
  5. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/payload.py +23 -4
  6. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/sap.py +30 -7
  7. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/sap_models.py +8 -2
  8. {pyxecm-3.8.0 → pyxecm-3.8.2}/.gitignore +0 -0
  9. {pyxecm-3.8.0 → pyxecm-3.8.2}/README.md +0 -0
  10. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/__init__.py +0 -0
  11. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/avts.py +0 -0
  12. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/coreshare.py +0 -0
  13. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/helper/__init__.py +0 -0
  14. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/helper/assoc.py +0 -0
  15. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/helper/data.py +0 -0
  16. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/helper/logadapter.py +0 -0
  17. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/helper/web.py +0 -0
  18. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/helper/xml.py +0 -0
  19. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otac.py +0 -0
  20. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otawp.py +0 -0
  21. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otca.py +0 -0
  22. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otcs.py +0 -0
  23. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otds.py +0 -0
  24. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otiv.py +0 -0
  25. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otkd.py +0 -0
  26. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otmm.py +0 -0
  27. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/otpd.py +0 -0
  28. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm/py.typed +0 -0
  29. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/__init__.py +0 -0
  30. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/__main__.py +0 -0
  31. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/app.py +0 -0
  32. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/auth/__init__.py +0 -0
  33. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/auth/functions.py +0 -0
  34. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/auth/models.py +0 -0
  35. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/auth/router.py +0 -0
  36. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/common/__init__.py +0 -0
  37. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/common/functions.py +0 -0
  38. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/common/metrics.py +0 -0
  39. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/common/models.py +0 -0
  40. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/common/router.py +0 -0
  41. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/settings.py +0 -0
  42. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/terminal/__init__.py +0 -0
  43. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/terminal/router.py +0 -0
  44. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_maintenance/__init__.py +0 -0
  45. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_maintenance/functions.py +0 -0
  46. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_maintenance/models.py +0 -0
  47. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_maintenance/router.py +0 -0
  48. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_otcs/__init__.py +0 -0
  49. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_otcs/functions.py +0 -0
  50. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_otcs/router.py +0 -0
  51. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_payload/__init__.py +0 -0
  52. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_payload/functions.py +0 -0
  53. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_payload/log_reader.py +0 -0
  54. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_payload/models.py +0 -0
  55. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_api/v1_payload/router.py +0 -0
  56. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/__init__.py +0 -0
  57. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/__main__.py +0 -0
  58. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/browser_automation.py +0 -0
  59. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/exceptions.py +0 -0
  60. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/k8s.py +0 -0
  61. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/log.py +0 -0
  62. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/m365.py +0 -0
  63. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/manager_push.py +0 -0
  64. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/payload_list.py +0 -0
  65. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/salesforce.py +0 -0
  66. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/servicenow.py +0 -0
  67. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/settings.py +0 -0
  68. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/successfactors.py +0 -0
  69. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/translate.py +0 -0
  70. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_customizer/version.py +0 -0
  71. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_maintenance_page/__init__.py +0 -0
  72. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_maintenance_page/__main__.py +0 -0
  73. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_maintenance_page/app.py +0 -0
  74. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_maintenance_page/settings.py +0 -0
  75. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_maintenance_page/static/favicon.avif +0 -0
  76. {pyxecm-3.8.0 → pyxecm-3.8.2}/src/pyxecm_maintenance_page/templates/maintenance.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyxecm
3
- Version: 3.8.0
3
+ Version: 3.8.2
4
4
  Summary: A Python library to interact with Opentext Content Management Rest API
5
5
  Project-URL: Homepage, https://github.com/opentext/pyxecm
6
6
  Author-email: Kai Gatzweiler <kgatzweiler@opentext.com>, "Dr. Marc Diefenbruch" <mdiefenb@opentext.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyxecm"
3
- version = "3.8.0"
3
+ version = "3.8.2"
4
4
  description = 'A Python library to interact with Opentext Content Management Rest API'
5
5
  keywords = [
6
6
  'opentext',
@@ -1792,6 +1792,7 @@ class Customizer:
1792
1792
  otca_object=self.otca_object,
1793
1793
  otkd_object=self.otkd_object,
1794
1794
  avts_object=self.avts_object,
1795
+ settings=self.settings,
1795
1796
  logger=self.logger,
1796
1797
  )
1797
1798
  # Load the payload file and initialize the payload sections:
@@ -1470,7 +1470,12 @@ class Guidewire:
1470
1470
 
1471
1471
  # end method definition
1472
1472
 
1473
- def search_policy(self, attributes: dict) -> dict:
1473
+ def search_policy(
1474
+ self,
1475
+ attributes: dict,
1476
+ filter_field: str | None = None,
1477
+ filter_value: str | None = None,
1478
+ ) -> dict:
1474
1479
  """Search a specific policy based on its attributes.
1475
1480
 
1476
1481
  See: https://docs.guidewire.com/cloud/pc/202407/apiref/generated/Policy%20API/search-policies--post
@@ -1488,6 +1493,10 @@ class Guidewire:
1488
1493
  * "firstName" (str)
1489
1494
  * "lastName" (str)
1490
1495
  * "officialId" (str)
1496
+ filter_field (str | None, optional):
1497
+ Additional policy attribute used to filter the results.
1498
+ filter_value (str | None, optional):
1499
+ Value that filter_field must equal.
1491
1500
 
1492
1501
  Returns:
1493
1502
  dict:
@@ -1528,7 +1537,17 @@ class Guidewire:
1528
1537
 
1529
1538
  request_url = self.config()["policySearchUrl"]
1530
1539
 
1531
- return self.do_request(method="POST", json_data=body, url=request_url)
1540
+ response = self.do_request(method="POST", json_data=body, url=request_url)
1541
+ policies = response.get("data", [])
1542
+ if filter_field and filter_value is not None and isinstance(policies, list):
1543
+ response["data"] = [
1544
+ policy
1545
+ for policy in policies
1546
+ if policy.get("attributes", {}).get(filter_field) == filter_value
1547
+ ]
1548
+ response["count"] = len(response["data"])
1549
+
1550
+ return response
1532
1551
 
1533
1552
  # end method definition
1534
1553
 
@@ -248,6 +248,7 @@ class Payload:
248
248
  otca_object: OTCA | None = None,
249
249
  otkd_object: OTKD | None = None,
250
250
  avts_object: AVTS | None = None,
251
+ settings: Settings | None = None,
251
252
  logger: logging.Logger = default_logger,
252
253
  ) -> None:
253
254
  """Initialize the Payload object.
@@ -306,6 +307,9 @@ class Payload:
306
307
  An optional Knowledge Discovery object.
307
308
  avts_object (AVTS | None, optional):
308
309
  An optional Aviator Search object.
310
+ settings (Settings | None, optional):
311
+ The customizer settings. They are the fallback for values that are
312
+ not defined in the payload. Defaults to the settings of the environment.
309
313
  logger (logging.Logger, optional):
310
314
  The logging object to use for all log messages. Defaults to default_logger.
311
315
 
@@ -315,6 +319,7 @@ class Payload:
315
319
  for logfilter in logger.filters:
316
320
  self.logger.addFilter(logfilter)
317
321
 
322
+ self._settings = settings if settings is not None else Settings()
318
323
  self._stop_on_error = stop_on_error
319
324
  self._payload_source = payload_source
320
325
  self._k8s = k8s_object
@@ -12152,6 +12157,8 @@ class Payload:
12152
12157
  object_type: str,
12153
12158
  search_field: str,
12154
12159
  search_value: str,
12160
+ filter_field: str | None = None,
12161
+ filter_value: str | None = None,
12155
12162
  ) -> str | None:
12156
12163
  """Get the Guidewire ID (str) of a Guidewire object by querying the Guidewire API.
12157
12164
 
@@ -12165,6 +12172,10 @@ class Payload:
12165
12172
  Search field to find business object in external system.
12166
12173
  search_value (str):
12167
12174
  Search value to find business object in external system.
12175
+ filter_field (str | None, optional):
12176
+ Policy attribute used to filter the search results.
12177
+ filter_value (str | None, optional):
12178
+ Value that filter_field must equal.
12168
12179
 
12169
12180
  Returns:
12170
12181
  str | None:
@@ -12207,6 +12218,8 @@ class Payload:
12207
12218
  case "Policy" | "policy" | "gw.policy":
12208
12219
  response = guidewire_object.search_policy(
12209
12220
  attributes={search_field: search_value},
12221
+ filter_field=filter_field,
12222
+ filter_value=filter_value,
12210
12223
  )
12211
12224
  bo_id = guidewire_object.get_result_value(response=response, key="policyNumber")
12212
12225
  case "Claim" | "claim" | "gw.claim":
@@ -12718,6 +12731,8 @@ class Payload:
12718
12731
  bo_id = business_object_data["bo_id"]
12719
12732
  bo_search_field = None
12720
12733
  bo_search_value = None
12734
+ bo_filter_field = None
12735
+ bo_filter_value = None
12721
12736
  elif "bo_search_field" not in business_object_data or "bo_search_value" not in business_object_data:
12722
12737
  self.logger.error(
12723
12738
  "Missing BO search fields (bo_search_field, bo_search_value) in Business Object payload without BO ID for workspace -> '%s'.",
@@ -12727,6 +12742,8 @@ class Payload:
12727
12742
  else:
12728
12743
  bo_search_field = business_object_data["bo_search_field"]
12729
12744
  bo_search_value = business_object_data["bo_search_value"]
12745
+ bo_filter_field = business_object_data.get("bo_filter_field")
12746
+ bo_filter_value = business_object_data.get("bo_filter_value")
12730
12747
  bo_id = None
12731
12748
 
12732
12749
  # Lookup external system in this payload or a former processed payload:
@@ -12811,6 +12828,8 @@ class Payload:
12811
12828
  object_type=bo_type,
12812
12829
  search_field=bo_search_field,
12813
12830
  search_value=bo_search_value,
12831
+ filter_field=bo_filter_field,
12832
+ filter_value=bo_filter_value,
12814
12833
  )
12815
12834
  if not bo_id:
12816
12835
  self.logger.warning(
@@ -20816,13 +20835,13 @@ class Payload:
20816
20835
  # Principal propagation (pyxecm as SAML 2.0 identity provider, see SAP.get_saml_assertion())
20817
20836
  # First priority is taking it from the external system payload - otherwise fall back to the global settings
20818
20837
  saml_idp_private_key_file = (
20819
- sap_external_system.get("saml_idp_private_key_file") or Settings.sap.saml_idp_private_key_file
20838
+ sap_external_system.get("saml_idp_private_key_file") or self._settings.sap.saml_idp_private_key_file
20820
20839
  )
20821
20840
  saml_idp_certificate_file = (
20822
- sap_external_system.get("saml_idp_certificate_file") or Settings.sap.saml_idp_certificate_file
20841
+ sap_external_system.get("saml_idp_certificate_file") or self._settings.sap.saml_idp_certificate_file
20823
20842
  )
20824
- saml_issuer = sap_external_system.get("saml_issuer") or Settings.sap.saml_issuer
20825
- saml_audience = sap_external_system.get("saml_audience") or Settings.sap.saml_audience
20843
+ saml_issuer = sap_external_system.get("saml_issuer") or self._settings.sap.saml_issuer
20844
+ saml_audience = sap_external_system.get("saml_audience") or self._settings.sap.saml_audience
20826
20845
  # Read the PEM files (typically mounted from a Kubernetes secret). The file names are
20827
20846
  # not checked (e.g. a Kubernetes TLS secret mounts "tls.key" / "tls.crt") - SAP()
20828
20847
  # validates the content and logs an error if it is not a matching PEM key pair:
@@ -976,6 +976,24 @@ class SAP:
976
976
 
977
977
  # end method definition
978
978
 
979
+ def oauth_token_url(self) -> str:
980
+ """Return the URL of SAP's OAuth 2.0 token endpoint, including the SAP client.
981
+
982
+ SAP checks that the Recipient of a SAML bearer assertion is exactly the URL the
983
+ token request is sent to - including the sap-client query parameter. Both
984
+ get_saml_assertion() and get_oauth_access_token() therefore use this URL.
985
+
986
+ Returns:
987
+ str:
988
+ The token endpoint URL, e.g.
989
+ "https://s4.example.com:44300/sap/bc/sec/oauth2/token?sap-client=100".
990
+
991
+ """
992
+
993
+ return "{}{}?{}".format(self._base_url, OAUTH_TOKEN_PATH, urllib.parse.urlencode({"sap-client": self._client}))
994
+
995
+ # end method definition
996
+
979
997
  def get_saml_assertion(
980
998
  self,
981
999
  user_id: str,
@@ -1023,8 +1041,9 @@ class SAP:
1023
1041
  Defaults to "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".
1024
1042
  recipient (str | None, optional):
1025
1043
  The token endpoint URL the assertion is posted to (SubjectConfirmationData
1026
- Recipient). Defaults to None = "<base_url>/sap/bc/sec/oauth2/token". Only
1027
- needed if SAP is reached via a different URL (e.g. a reverse proxy).
1044
+ Recipient). Defaults to None = oauth_token_url(), i.e.
1045
+ "<base_url>/sap/bc/sec/oauth2/token?sap-client=<client>". Only needed if SAP
1046
+ sees the token request under a different URL (e.g. behind a reverse proxy).
1028
1047
  audience (str | None, optional):
1029
1048
  The AudienceRestriction. Defaults to None = saml_audience of the constructor.
1030
1049
  validity_seconds (int, optional):
@@ -1073,7 +1092,7 @@ class SAP:
1073
1092
  )
1074
1093
  return None
1075
1094
 
1076
- recipient = recipient or (self._base_url + OAUTH_TOKEN_PATH)
1095
+ recipient = recipient or self.oauth_token_url()
1077
1096
  audience = audience or self._saml_audience
1078
1097
 
1079
1098
  now = datetime.now(tz=UTC)
@@ -1167,7 +1186,7 @@ class SAP:
1167
1186
  ("Authorization: Bearer <access_token>", see the 'auth' parameter of the SAP class)
1168
1187
  run with the SAP user's own authorizations - no SAP password of the user is needed.
1169
1188
 
1170
- The request is sent as form data to <base_url>/sap/bc/sec/oauth2/token. It does
1189
+ The request is sent as form data to oauth_token_url(). It does
1171
1190
  not use the session of this object (the token endpoint authenticates the OAuth
1172
1191
  client, not the technical user).
1173
1192
 
@@ -1219,7 +1238,8 @@ class SAP:
1219
1238
  if assertion.lstrip().startswith("<"):
1220
1239
  assertion = base64.urlsafe_b64encode(assertion.strip().encode("utf-8")).decode("ascii").rstrip("=")
1221
1240
 
1222
- token_url = self._base_url + OAUTH_TOKEN_PATH
1241
+ # Same URL (incl. sap-client) as the default Recipient of the assertion - SAP compares both:
1242
+ token_url = self.oauth_token_url()
1223
1243
  data = {"grant_type": OAUTH_SAML2_BEARER_GRANT_TYPE, "assertion": assertion, "client_id": client_id}
1224
1244
  if scope:
1225
1245
  data["scope"] = scope
@@ -1229,7 +1249,6 @@ class SAP:
1229
1249
  try:
1230
1250
  response = requests.post(
1231
1251
  url=token_url,
1232
- params={"sap-client": self._client},
1233
1252
  data=data,
1234
1253
  auth=(client_id, client_secret),
1235
1254
  headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
@@ -1278,6 +1297,7 @@ class SAP:
1278
1297
  client_secret: str,
1279
1298
  scope: str | None = None,
1280
1299
  nameid_format: str = SAML_NAMEID_FORMAT_UNSPECIFIED,
1300
+ recipient: str | None = None,
1281
1301
  ) -> dict | None:
1282
1302
  """Get an SAP OAuth 2.0 access token for a user via principal propagation, in one call.
1283
1303
 
@@ -1302,6 +1322,9 @@ class SAP:
1302
1322
  nameid_format (str, optional):
1303
1323
  The NameID format - must match transaction SAML2 (see get_saml_assertion()).
1304
1324
  Defaults to "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".
1325
+ recipient (str | None, optional):
1326
+ The Recipient of the assertion - only needed behind a reverse proxy (see
1327
+ get_saml_assertion()). Defaults to None = oauth_token_url().
1305
1328
 
1306
1329
  Returns:
1307
1330
  dict | None:
@@ -1310,7 +1333,7 @@ class SAP:
1310
1333
 
1311
1334
  """
1312
1335
 
1313
- assertion = self.get_saml_assertion(user_id=user_id, nameid_format=nameid_format)
1336
+ assertion = self.get_saml_assertion(user_id=user_id, nameid_format=nameid_format, recipient=recipient)
1314
1337
  if not assertion:
1315
1338
  return None
1316
1339
 
@@ -1255,7 +1255,7 @@ class SAPMaintenanceOrderModel(SAPODataCreateModel):
1255
1255
  basic_start_datetime: ODataV2DateTime = Field(
1256
1256
  default=None,
1257
1257
  alias="MaintOrdBasicStartDateTime",
1258
- description="Basic start date and time of the order, ISO 8601, e.g. '2026-11-01T08:00:00Z'; without timezone treated as UTC.",
1258
+ description="Basic start date and time of the order, ISO 8601, e.g. '2026-11-01T08:00:00Z'; without timezone treated as UTC. Defaults to now if neither start nor end is given.",
1259
1259
  )
1260
1260
  basic_end_datetime: ODataV2DateTime = Field(
1261
1261
  default=None,
@@ -1271,7 +1271,13 @@ class SAPMaintenanceOrderModel(SAPODataCreateModel):
1271
1271
 
1272
1272
  @model_validator(mode="after")
1273
1273
  def check_dates(self) -> Self:
1274
- """Validate that the basic end date is not before the basic start date."""
1274
+ """Default the basic start date and validate that the basic end date is not before it."""
1275
+
1276
+ # The API does not default the basic dates - without them SAP rejects the order with
1277
+ # IW/426 "Forward scheduling: Please enter start date". An end date alone is left
1278
+ # as is (backward scheduling):
1279
+ if self.basic_start_datetime is None and self.basic_end_datetime is None:
1280
+ self.basic_start_datetime = datetime.now(tz=UTC).replace(microsecond=0)
1275
1281
 
1276
1282
  # Compare as UTC timestamps (naive datetimes are treated as UTC, see serialize_odata_datetime()):
1277
1283
  start, end = (
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes