From 66a26cc85e9899100b874d303fb42264e5123acb Mon Sep 17 00:00:00 2001 From: danfunk Date: Fri, 4 Aug 2023 14:16:40 -0400 Subject: [PATCH] does adding a new check prevent an error that only seems to happen whenthe frontend is engaged --- app/spiffworkflow/messages/MessageHelpers.js | 16 +- test/spec/bpmn/request_new_role.bpmn | 2984 ++++++++++++++++++ 2 files changed, 2997 insertions(+), 3 deletions(-) create mode 100644 test/spec/bpmn/request_new_role.bpmn diff --git a/app/spiffworkflow/messages/MessageHelpers.js b/app/spiffworkflow/messages/MessageHelpers.js index 9c4e6fb..dd06efa 100644 --- a/app/spiffworkflow/messages/MessageHelpers.js +++ b/app/spiffworkflow/messages/MessageHelpers.js @@ -148,14 +148,24 @@ function getRetrievalExpressionFromCorrelationProperty( export function findCorrelationProperties(businessObject, moddle) { const root = getRoot(businessObject, moddle); const correlationProperties = []; - for (const rootElement of root.rootElements) { - if (rootElement.$type === 'bpmn:CorrelationProperty') { - correlationProperties.push(rootElement); + if (isIterable(root.rootElements)) { + for (const rootElement of root.rootElements) { + if (rootElement.$type === 'bpmn:CorrelationProperty') { + correlationProperties.push(rootElement); + } } } return correlationProperties; } +function isIterable(obj) { + // checks for null and undefined + if (obj == null) { + return false; + } + return typeof obj[Symbol.iterator] === 'function'; +} + export function findCorrelationKeys(businessObject, moddle) { const root = getRoot(businessObject, moddle); const correlationKeys = []; diff --git a/test/spec/bpmn/request_new_role.bpmn b/test/spec/bpmn/request_new_role.bpmn new file mode 100644 index 0000000..c34d2e3 --- /dev/null +++ b/test/spec/bpmn/request_new_role.bpmn @@ -0,0 +1,2984 @@ + + + + + + + + + Gateway_1bz36qs + Gateway_1ci8c4i + Event_15gc86r + Event_1lr6j50 + Gateway_0dvdlwh + Gateway_0gpvfm8 + Gateway_0z3562z + Activity_05solt4 + Activity_05ou4v1 + Activity_1vw9byu + Activity_0o5rhui + Activity_1e64lzx + Activity_1dpvgq0 + + + Activity_0h7ft7p + Gateway_0ip2pbu + Activity_0apmo1t + Activity_0adfss7 + Activity_00bqo01 + + + Activity_04ium7h + Gateway_09pngbv + + + Event_14e2or3 + Activity_1el69ef + Activity_0cac2ge + + + + + + + Flow_1rpyrgl + Flow_0m414q7 + Flow_154casc + + + Flow_1gpngr0 + Flow_1g8h223 + Flow_0xz0rte + + + Flow_1awy4f9 + + + + Process Recap +--- +The request has been cancelled and will be removed from Instances waiting on you. + +**Status**: {{ trvl_details["status"] }} + +**Requestor**: {{ trvl_details["requestor"] }} + + Flow_1g8h223 + + + Flow_1ieb889 + Flow_0dxc4uz + Flow_1r7byc8 + + + Flow_0idzytl + Flow_0m414q7 + Flow_0mi1iph + + + Flow_1r7byc8 + Flow_1hb6m2j + Flow_1svj7af + + + Flow_0xz0rte + Flow_0nom5oy + # Set/Clear Last Approval Outcome +last_approval_outcome = "" + +# Set Approval Type Agnostic Requestor Authentication Username +requestor_username = trvl_details["requestor_auth_username"] +cc_requested_for_username = trvl_details["core_contributor_work_email"] + +# Set variables for Waku messages +waku_msg_project = trvl_details["project_name"] +waku_msg_category = trvl_details["category_name"] +waku_msg_total_amount = trvl_details["total_request_amount_usd"] +waku_msg_criticality = trvl_details["criticality"] +waku_msg_requestor = trvl_details["requestor"] + +# Delete Unneeded variables +del(is_request_cancelled) + + + + # Set Current Role +current_approver_role = "Budget Owner" + +# Set Current Requestor Bamboo API ID +requestor_bamboo_eid = trvl_details["requestor_bamboo_eid"] + # Clear Last Approval Role +del(current_approver_role) + +# Resat Feviewer Outcome +reviewer_outcome = "" + + + Flow_0nom5oy + Flow_1ieb889 + + + + # Set Current Role +current_approver_role = "PeopleOps Partner" + +# Default Escalation to Budget Owner to False +is_escalate = False + # Clear Last Approval Role +del(current_approver_role) + +# Resat Feviewer Outcome +reviewer_outcome = "" + + Flow_0dxc4uz + Flow_1hb6m2j + + + Flow_1awy4f9 + Flow_1rpyrgl + + + + + + Flow_1k6ur0k + Flow_0c9yzka + + + Flow_1k6ur0k + + + + + + + Flow_0c9yzka + Flow_1n7gshu + # Create Details Dictionary +trvl_details = {} + +# Set Requestor First, Last and Preferred Names +trvl_details["requestor_first_name"] = bamboo_get_employee["firstName"] +trvl_details["requestor_last_name"] = bamboo_get_employee["lastName"] +trvl_details["requestor_preferred_name"] = bamboo_get_employee["preferredName"] + +# Set Requestor Full and Greet Names +if bamboo_get_employee["preferredName"] is None: + trvl_details["requestor"] = bamboo_get_employee["firstName"] + " " + bamboo_get_employee["lastName"] + trvl_details["requestor_greet_name"] = bamboo_get_employee["firstName"] + +else: + trvl_details["requestor"] = bamboo_get_employee["preferredName"] + " " + bamboo_get_employee["lastName"] + trvl_details["requestor_greet_name"] = bamboo_get_employee["preferredName"] + +# Set Approval Source Agnostic Requestor Greet Name +requestor_greet_name = trvl_details["requestor_greet_name"] + +# Set Requestor Ids +trvl_details["requestor_status_key"] = bamboo_get_employee["customStatusPublicKey"] +trvl_details["requestor_bamboo_eid"] = bamboo_get_employee["id"] +trvl_details["requestor_auth_username"] = auth_user_info["username"] + +# Set Default Core Contributor Request is for to Requestor +# trvl_details["core_contributor_test"] = r'{\"id\":\"' + trvl_details["requestor_bamboo_eid"] + r'\",\"last_name\":\"' + trvl_details["requestor_last_name"] + r'\",\"first_name\":\"' + trvl_details["requestor_first_name"] + r'\"}' +req_as_cc = {} +req_as_cc["id"] = trvl_details["requestor_bamboo_eid"] +req_as_cc["last_name"] = trvl_details["requestor_last_name"] +if bamboo_get_employee["preferredName"] is None: + req_as_cc["first_name"] = trvl_details["requestor_first_name"] +else: + req_as_cc["first_name"] = trvl_details["requestor_first_name"] + " (" + trvl_details["requestor_preferred_name"] + ")" +req_as_cc["workEmail"] = bamboo_get_employee["workEmail"] + +trvl_details["core_contributor"] = json.dumps(req_as_cc) + +# Delete Unneeded Variables +del( bamboo_get_employee) +del(auth_user_info) + +# Initialize Approver List +approvers = [] + +# Initialize Approval History +approval_history = [] + +# Set Approveal Source +approval_source = "TRVL" + +# Set Reviewer Comments List +review_comment_list = [] +review_comment_cnt = 0 + +# Set Defalt reviewer Comment +nai_reviewer_comment = {} +nai_reviewer_comment["Reviewer Comment"] = "" + +# Set Frontend URLs +prod_frontend_url = "https://prod.mod.spiff.status.im" + +# Add additional variables +is_additional_information = False +is_supporting_files = False +is_supporting_information = False +requestor_edit_option = False +non_book_check_cnt = 0 +last_approval_outcome = "" + + + Flow_1n7gshu + + + + + + + # Delete Unneeded Variables +del(travel_sub_category_enum_list) +del(currency_type_enum_list) +del(currency_enum_list) +del(dept_enum_list) +del(category_enum_list) +del(event_type_enum_list) + # Initiate Request Cancelled to False +is_request_cancelled = False + + Flow_154casc + Flow_1gpngr0 + + Flow_13sdhgh + Flow_0fhz1yp + + Flow_0rt9rpn + + + Flow_0rt9rpn + Flow_1yboqgc + Flow_1regjqs + Flow_0ijiqtp + Flow_1lvqe7v + Flow_0org7jx + Flow_1e8hadk + + + Flow_0rydcxn + Flow_1ihkyvi + Flow_0zaclsu + Flow_0h3piq7 + Flow_1dcxvqq + Flow_18n2gwv + Flow_0n82r7f + + + Get Form Data Call Activity is used for getting required data for User Tasks. + + # Set Task Name +which_bamboo_depart_div_data_rule = "Department Enum List" + # Exclude non-Project departments +dept_enum_list = [x for x in dept_enum_list if x["value"] not in ["18589","19125","19068","19110","19111","19126","19032","18737","19004","19147","19009","19033","19115"]] + +del(which_bamboo_depart_div_data_rule) + Loading Projects from Bamboo + + Flow_1yboqgc + Flow_0rydcxn + + + + + + + + # Set Enum List Parameters +whichSubject = "Sub-Category" +whichGrouping = "travel" +addNew = False +addBlank = False + # Copy to Category Enum List +travel_sub_category_enum_list = enumerations_list + +# Remove Meals Sub-category +travel_sub_category_enum_list = [x for x in travel_sub_category_enum_list if x["value"] != "meals"] + +# Delete generic list +del(enumerations_list) +del(whichSubject) +del(whichGrouping) +del(addNew) +del(addBlank) + + Flow_1regjqs + Flow_0zaclsu + + + + + Flow_1m73brt + + + Flow_0n82r7f + Flow_1m73brt + # Set Currency Type Enum List +currency_type_enum_list = [ + { + "label": "Crypto", + "value": "crypto" + }, + { + "label": "Fiat", + "value": "fiat" + } +] + +no_budget_owners_proceed = "" + +# Set Travel Category Enum List +category_enum_list = [ + { + "label": "Travel", + "value": "travel" + } +] + + + + + + # Set Enum List Parameters +whichSubject = "Event Type" +whichGrouping = "" +addNew = False +addBlank = False + # Copy to Category Enum List +event_type_enum_list = enumerations_list + +# Delete generic list +del(enumerations_list) +del(whichSubject) +del(whichGrouping) +del(addNew) +del(addBlank) + + Flow_1lvqe7v + Flow_0h3piq7 + + + + + + currency_enum_list = [ + { + "label": "United Arab Emirates Dirham (AED)", + "value": "AED" + }, + { + "label": "Australian Dollar (AUD)", + "value": "AUD" + }, + { + "label": "Canadian Dollar (CAD)", + "value": "CAD" + }, + { + "label": "Swiss Franc (CHF)", + "value": "CHF" + }, + { + "label": "Chinese Yuan (CNY)", + "value": "CNY" + }, + { + "label": "Colombian Peso (COP)", + "value": "COP" + }, + { + "label": "Euro (EUR)", + "value": "EUR" + }, + { + "label": "British Pound (GBP)", + "value": "GBP" + }, + { + "label": "Hong Kong Dollar (HKD)", + "value": "HKD" + }, + { + "label": "Japanese Yen (JPY)", + "value": "JPY" + }, + { + "label": "New Zealand Dollar (NZD)", + "value": "NZD" + }, + { + "label": "Singapore Dollar (SGD)", + "value": "SGD" + }, + { + "label": "United States Dollar (USD)", + "value": "USD" + } + ] + + + + Loading currencies from Xero + + Flow_0ijiqtp + Flow_1ihkyvi + + + Flow_0org7jx + Flow_1dcxvqq + + + Flow_1e8hadk + Flow_18n2gwv + + + + + + + + Departments, Divisions + + + temp fix + + + + Create a list of Programs, list of Projects + + + + PostgresDB or Spiff? + + + + Talent Level Guidance - to make it a step within the process + + + + Suggest it will be hard-coded in process model + + + + Connect to MercerDB? + + + + Can we use a spreadsheet as a source of truth? + + + + + Flow_0fhz1yp + Flow_1e5xai6 + Flow_1w54t94 + Flow_0dgrlbd + + + Flow_0zen0hg + Flow_1wk4ctc + + + Flow_1uda7lq + Flow_1b7wncj + + + Flow_18911q5 + Flow_1wixb6f + + + + Flow_13sdhgh + + + + + + + Flow_17xdzln + + + + Flow_1wixb6f + Flow_0zb8ksl + Flow_17xdzln + + + + Flow_0gs9a10 + Flow_0zb8ksl + # Set Request to Cancelled +is_request_cancelled = True + +# Set Final Status to Cancelled +trvl_details["status"] = "Cancelled" + + + + **Figma - New Demand request (Procurement) - SUBMIT**, https://www.figma.com/file/9NP2BUoLuwHUCGStvDMgOw/Form?node-id=0%3A1&t=BABeo58RkNviJr4N-0 + +**Fields details** - https://docs.google.com/spreadsheets/d/19S85IJeNXffPa1oAe6kSkmTleMWE92A4kWJFFodCEZU/edit#gid=1736626389 + + + + + + --- +Please ensure to provide all the necessary details regarding your travel request. These details are crucial for the Project Leads and PeopleOps Partners to review your request and make the necessary arrangements. + +If you need guidance on the travel policy, it may be helpful to check <a href="https://contributors.status.im/contributing-to-status/travelling-for-status.html" target="_blank" >Contributors Guide</a>. + +To get an overview of the process, you can check the process model or read <a href="https://www.notion.so/Request-Travel-06db3450784e4320adb425688d8dbe3f?pvs=4" target="_blank" >Process Description</a>. + +--- + +{% if is_additional_information %} +### Reviewer Additional Information Request + +| Requested by | <div style="width:90px">Date<div> | <div style="width:65px">Time<div> | Additional Information Requested | +| ----------------- | ----- | ----- | -------------------------------------- | +| {{ nai_reviewer_comment["Reviewer Role"] }} | {{ nai_reviewer_comment["Reviewer Comment Date"] }} | {{ nai_reviewer_comment["Reviewer Comment Time"] }} | {{ nai_reviewer_comment["Reviewer Comment"] }} | + + +--- + +{% endif %} + # Get Frontend URL +frontend_url = get_frontend_url() + +# Set BO check variable +budget_owner_check = trvl_details["project"] + +# Get Project Label +temp_project_name = [x["label"] for x in dept_enum_list if x["value"] == trvl_details["project"]] +trvl_details["project_name"] = temp_project_name[0] +del(temp_project_name) + +# Get Category Label +temp_category_name = [x["label"] for x in category_enum_list if x["value"] == trvl_details["category"]] +trvl_details["category_name"] = temp_category_name[0] +del(temp_category_name) + +# Get Event Type Label +temp_event_type_name = [x["label"] for x in event_type_enum_list if x["value"] == trvl_details["event_type"]] +trvl_details["event_type_name"] = temp_event_type_name[0] +del(temp_event_type_name) + +# Set Period (Event Date) Display Date Format +trvl_details["period_display"] = trvl_details["period"][8:10] + "-" + trvl_details["period"][5:7] + "-" + trvl_details["period"][:4] + +# Remove any linefeeds from from Purpose +purpose_double = trvl_details["purpose"] +purpose_single = purpose_double.replace("\n\n", " ") +purpose_removed = purpose_single.replace("\n", " ") +trvl_details["purpose"] = purpose_removed +del(purpose_double) +del(purpose_single) +del(purpose_removed) + +# Set Period (Start Date) Display Date Format +trvl_details["start_date_display"] = trvl_details["start_date"][8:10] + "-" + trvl_details["start_date"][5:7] + "-" + trvl_details["start_date"][:4] + +# Set Period (End Date) Display Date Format +trvl_details["end_date_display"] = trvl_details["end_date"][8:10] + "-" + trvl_details["end_date"][5:7] + "-" + trvl_details["end_date"][:4] + +# Get Frontend URL +frontend_url = get_frontend_url() + +# Set BO check variable +budget_owner_check = trvl_details["project"] + +# Set Edit Option to True +requestor_edit_option = True + +# Set Event Destination Display +csc_json_string_event = trvl_details["event_destination"] +csc_dict_event = json.loads(csc_json_string_event) +city_event = csc_dict_event["name"] +state_event = csc_dict_event["state"] +country_event = csc_dict_event["country"] +trvl_details["event_destination_display"] = city_event + " (" + state_event + ", " + country_event + ")" + +# Set Departure From Display +csc_json_string_dep = trvl_details["departure_from"] +csc_dict_dep = json.loads(csc_json_string_dep) +city_dep = csc_dict_dep["name"] +state_dep = csc_dict_dep["state"] +country_dep = csc_dict_dep["country"] +trvl_details["departure_from_display"] = city_dep + " (" + state_dep + ", " + country_dep + ")" + +# Set Return To Display +csc_json_string_ret = trvl_details["return_to"] +csc_dict_ret = json.loads(csc_json_string_ret) +city_ret = csc_dict_ret["name"] +state_ret = csc_dict_ret["state"] +country_ret = csc_dict_ret["country"] +trvl_details["return_to_display"] = city_ret + " (" + state_ret + ", " + country_ret + ")" + +# Set Core Contributor Values and Display +csc_json_string_cc = trvl_details["core_contributor"] +csc_dict_cc = json.loads(csc_json_string_cc) +last_name_cc = csc_dict_cc["last_name"] +first_name_cc = csc_dict_cc["first_name"] +trvl_details["core_contributor_display"] = first_name_cc + " " + last_name_cc +trvl_details["core_contributor_id"] = csc_dict_cc["id"] +trvl_details["core_contributor_work_email"] = csc_dict_cc["workEmail"] + +# Set Visa display - Replace True or False with actual value +trvl_details["visa_display"] = "" +if trvl_details["visa"] == True: + trvl_details["visa_display"] = "Yes" +else: + trvl_details["visa_display"] = "No" + +del(city_event) +del(state_event) +del(country_event) +del(csc_dict_event) +del(csc_json_string_event) +del(city_dep) +del(state_dep) +del(country_dep) +del(csc_dict_dep) +del(csc_json_string_dep) +del(city_ret) +del(state_ret) +del(country_ret) +del(csc_dict_ret) +del(csc_json_string_ret) +del(csc_json_string_cc) +del(csc_dict_cc) +del(last_name_cc) +del(first_name_cc) + + + Flow_1sx4un4 + Flow_0xus3om + + + Flow_0xus3om + Flow_0ch4cxy + Flow_0op21ag + + + + # Delete Unneeded Variable +del(frontend_url) + + Flow_0op21ag + Flow_119j0el + + + + # Delete Unneeded Variable +del(frontend_url) + + Flow_0ch4cxy + Flow_18qe7ut + + + Flow_05hzfur + Flow_1whl6cg + + + # Set Xe Conversion Parameters +xe_convert_from_currency = trvl_details["item"][item_list_cnt]["currency"] +xe_convert_to_currency = "USD" +xe_amount = trvl_details["item"][item_list_cnt]["unit_price_num"] +xe_decimal_places = 2 + # Delete Unneeded Variables +del(xe_convert_from_currency) +del(xe_convert_to_currency) +del(xe_amount) +del(xe_decimal_places) + Getting currency rates from XE + + Flow_0oj2rzg + Flow_0yzymjg + + + Flow_04gxo4i + Flow_0e95pd0 + # Set List Count and Counter +item_list_cnt = 0 +item_list_total = len(trvl_details["item"]) + +# Initially set Request Exceeds Threshold to False +request_exceeds_threshold = False + +# Set Default Value for Crypto Conversion Check +is_xe = False + +# Set Item Name and URl lists +is_item_url_list = [] +item_url_list = [] +item_name_list = [] + + + Flow_11jp113 + Flow_1s5j2un + # Get Process Instance +process_info = get_toplevel_process_info() +process_instance_id = process_info["process_instance_id"] +process_instance_id_str = str(process_instance_id) +del(process_info) + +# Add Process Instance Id, for Postgres extract +trvl_details["item"][item_list_cnt]["process_instance_id"] = process_instance_id_str + +# If selected, add Fiat conversion info +if trvl_details["item"][item_list_cnt]["currency_type"] == "fiat": + trvl_details["item"][item_list_cnt]["converted_to_unit_price"] = round(xe_convert_from["to"][0]["mid"], 2) + trvl_details["item"][item_list_cnt]["xe_usd_mid"] = xe_convert_from["to"][0]["mid"] + trvl_details["item"][item_list_cnt]["unit_price_total"] = round(trvl_details["item"][item_list_cnt]["qty"] * trvl_details["item"][item_list_cnt]["unit_price_num"], 2) + trvl_details["item"][item_list_cnt]["xe_usd_mid_date"] = local_date_str + trvl_details["item"][item_list_cnt]["cg_usd_mid_time"] = local_time_str + del(xe_convert_from) + +# If selected, add Crypto conversion info +if trvl_details["item"][item_list_cnt]["currency_type"] == "crypto": + trvl_details["item"][item_list_cnt]["converted_to_unit_price"] = round(cg_usd_conversion_rate * trvl_details["item"][item_list_cnt]["unit_price_num"], 4) + trvl_details["item"][item_list_cnt]["cg_usd_conversion_rate"] = cg_usd_conversion_rate + trvl_details["item"][item_list_cnt]["unit_price_total"] = round(trvl_details["item"][item_list_cnt]["qty"] * trvl_details["item"][item_list_cnt]["unit_price_num"], 4) + trvl_details["item"][item_list_cnt]["cg_usd_conversion_rate_date"] = local_date_str + trvl_details["item"][item_list_cnt]["cg_usd_conversion_rate_time"] = local_time_str + +# Calculate Converted to unit price total +trvl_details["item"][item_list_cnt]["converted_to_currency"] = "USD" +trvl_details["item"][item_list_cnt]["converted_to_unit_price_total"] = round(trvl_details["item"][item_list_cnt]["qty"] * trvl_details["item"][item_list_cnt]["converted_to_unit_price"], 2) + +# Delete Unneeded Variables +del(local_date_str) +del(local_time_str) + + + Flow_04gxo4i + + + Flow_1fr8h2a + Flow_03hk9xl + Flow_0wmxs2e + + + Flow_0e95pd0 + Flow_0wmxs2e + Flow_0r6exl3 + + + Flow_08d1pda + Flow_1c12pyi + # Set Item Unit Price to Numeric Value +trvl_details["item"][item_list_cnt]["unit_price_num"] = float((trvl_details["item"][item_list_cnt]["unit_price"]).replace(',', '')) + +# Set Sub-Category Name +temp_sub_category_name = [x["label"] for x in travel_sub_category_enum_list if x["value"] == trvl_details["item"][item_list_cnt]["sub_category"]] + +trvl_details["item"][item_list_cnt]["sub_category_name"] = temp_sub_category_name[0] +del(temp_sub_category_name) + +# Get Currency Type Label +temp_currency_type_name = [x["label"] for x in currency_type_enum_list if x["value"] == trvl_details["item"][item_list_cnt]["currency_type"]] +trvl_details["item"][item_list_cnt]["currency_type_name"] = temp_currency_type_name[0] +del(temp_currency_type_name) + + + Flow_11bg9z9 + Flow_077coya + # Calculate total UDS anount +converted_to_unit_price_total_sum = sum(v.get('converted_to_unit_price_total', 0) for v in trvl_details["item"]) +trvl_details["meals_cost"] +converted_to_unit_price_total_sum_rounded = round(converted_to_unit_price_total_sum, 2) +trvl_details["total_request_amount_usd"] = converted_to_unit_price_total_sum_rounded +trvl_details["total_request_amount_usd_display"] = "{:,.2f}".format(trvl_details["total_request_amount_usd"]) + +# Delete Unneeded Variable +if is_xe: + del(xe_amount) + del(xe_convert_from) + del(xe_convert_from_currency) + del(xe_convert_to_currency) + del(xe_decimal_places) + +del(exceeds_threshold) +del(item_list_cnt) +del(is_xe) +del(converted_to_unit_price_total_sum) +del(converted_to_unit_price_total_sum_rounded) +del(process_instance_id_str) + + + Flow_1c12pyi + Flow_0oj2rzg + Flow_0y3b16d + + + Flow_0yzymjg + Flow_1gdgdff + Flow_0mj9wyr + + + + # Set Convert From Coin Symbol +coin_symbol = trvl_details["item"][item_list_cnt]["currency"] + + Getting currency rates from CoinGecko + + Flow_0y3b16d + Flow_1gdgdff + + + + # Set input variables +threshold_category = trvl_details["category"] +threshold_sub_category = trvl_details["item"][item_list_cnt]["sub_category"] +exceeds_check_unit_price = trvl_details["item"][item_list_cnt]["converted_to_unit_price"] + # Set Threshold +trvl_details["item"][item_list_cnt]["exceeds_threshold"] = exceeds_threshold + +# If any item exceeds threshold, request exceeds threshold +if exceeds_threshold: + request_exceeds_threshold = True + +# Increase Item Id +trvl_details["item"][item_list_cnt]["item_id"] = item_list_cnt + 1 + +# Increase Item Count +item_list_cnt = item_list_cnt + 1 + +# Delete variables +del(threshold_category) +del(threshold_sub_category) +del(exceeds_check_unit_price) + + Flow_1s5j2un + Flow_1fr8h2a + + + Flow_0mj9wyr + Flow_11jp113 + + + Flow_077coya + + + Flow_03hk9xl + Flow_11bg9z9 + # Calculate total per diem meal cost +start_object = datetime.strptime(trvl_details["start_date"], '%Y-%m-%d') +end_object = datetime.strptime(trvl_details["end_date"], '%Y-%m-%d') +number_of_days = (end_object - start_object).days + 1 +trvl_details["number_of_days"] = number_of_days + +# Meals in USD +meals_cost = number_of_days * 75 +trvl_details["meals_cost"] = meals_cost +trvl_details["meals_cost_display"] = "{:,.2f}".format(trvl_details["meals_cost"]) + +del(start_object) +del(end_object) +del(meals_cost) +del(number_of_days) + + + trvl_details["item"][item_list_cnt]["currency_type"] == "fiat" + + + + + + + + + + item_list_cnt < item_list_total + + + + + + trvl_details["item"][item_list_cnt]["currency_type"] == "crypto" + + + + + + + + Flow_0r6exl3 + Flow_08d1pda + # Check for Item URl +if "item_url" in trvl_details["item"][item_list_cnt]: + is_item_url_list.append(True) + item_url_list.append(trvl_details["item"][item_list_cnt]["item_url"]) +else: + is_item_url_list.append(False) + item_url_list.append("") + +# Set Ite m Name +item_name_list.append(trvl_details["item"][item_list_cnt]["item_name"]) + + + + + + Flow_119j0el + Flow_18qe7ut + Flow_0y8xob6 + + + Flow_0y8xob6 + Flow_19t1udw + Flow_05hzfur + + + + frontend_url == "https://prod.spiffworkflow.org" + + + + l1_budget_owner_bamboo_eid == 0 or l2_budget_owner_bamboo_eid == 0 + + + + + + + + + Cancel Request + + Flow_0z8xl4u + + + + + + + + + --- +The Project you selected is not configured for the approval process at this time. Click Continue to select a different Project or Cancel the Request. + +--- + + Flow_19t1udw + Flow_1e5xai6 + + + + + Cancel Request + + Flow_0zen0hg + + + + + + + + + --- + +**Requestor**: {{ trvl_details["requestor"] }} + +--- + +| | | +| --------------: | -------------------------------- | +|**Traveller** | {{ trvl_details["core_contributor_display"] }} | +|**Purpose** | {{ trvl_details["purpose"] }} | + +| Project | Event Type | Criticality | Event Date | Event Name | Event Destination | Visa required? | +| -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | ----- | +| {{ trvl_details["project_name"] }} | {{ trvl_details["event_type_name"] }} | {{ trvl_details["criticality"] }} | {{ trvl_details["period_display"] }} | {{ trvl_details["event_name"] }} | {{ trvl_details["event_destination_display"] }} | {{ trvl_details["visa_display"] }} | + +---------- +### Itinerary + +**Departure**: {{ trvl_details["start_date_display"] }} , {{ trvl_details["departure_from_display"] }} + +**Return**: {{ trvl_details["end_date_display"] }}, {{ trvl_details["return_to_display"] }} + +**Number of days**: {{ trvl_details["number_of_days"] }} + +--- +### Items +| Item | Sub-Category | Qty | Currency | Unit Price | Total Price | USD Price | Total USD Price | +| ---- | ----------------- | :---: | :---------: | ----------: | ------------: | -----------: | -----------------: | +| Per diem | Meals | {{ trvl_details["number_of_days"] }} | USD | 75.00 | {{ trvl_details["meals_cost_display"] }} | 75.00 | {{ trvl_details["meals_cost_display"] }} | +{% for icnt in range(item_list_total) %} +| {% if is_item_url_list[icnt] %}<a href="{{ item_url_list[icnt] }}" target="_blank" >{{ item_name_list[icnt] }}</a>{% else %}{{ item_name_list[icnt] }}{% endif %} | {{ trvl_details["item"][icnt]["sub_category_name"] }} | {{ trvl_details["item"][icnt]["qty"] }} | {{ trvl_details["item"][icnt]["currency"] }} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% endif %} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% endif %} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price"]) }} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price_total"]) }} | +{% endfor %} +---------- +### Total Request: {{ "{:,.2f}".format(trvl_details["total_request_amount_usd"]) }} USD** + + +<sub> *Fiat - Exchange Rates used to calculate the "USD" amounts are under <a href="http://www.xe.com/" target="_blank" >license from Xe</a>. Please note that if you use this Xe Data, you are obliged to comply with <a href="http://www.xe.com/legal/dfs.php" target="_blank" >Xe's end terms of use</a>. <sub> + +<sub> *Crypto - Exchange Rates used to calculate the "USD" amounts are received from <a href="https://www.coingecko.com/en/api" target="_blank" >CoinGecko</a>.<sub> + +---------- +{% if is_additional_information %} +### Reviewer Additional Information Request + +| Requested by | <div style="width:90px">Date<div> | <div style="width:65px">Time<div> | Additional Information Requested | +| ----------------- | ----- | ----- | -------------------------------------- | +| {{ nai_reviewer_comment["Reviewer Role"] }} | {{ nai_reviewer_comment["Reviewer Comment Date"] }} | {{ nai_reviewer_comment["Reviewer Comment Time"] }} | {{ nai_reviewer_comment["Reviewer Comment"] }} | + + +--- + +{% endif %} + # If no addtion details were provided, set more_details to blank +try: + supporting_information +except NameError: + supporting_information = "" + +# Determine if additional details were provided +is_supporting_information = len(supporting_information) > 0 + +# if Supporting INformation, Remove any linefeeds +if is_supporting_information: + supporting_information_double = supporting_information + supporting_information_single = supporting_information_double.replace("\n\n", " ") + supporting_information = supporting_information_single.replace("\n", " ") + del(supporting_information_double) + del(supporting_information_single) + +# Add Supporting Information to Details +trvl_details["supporting_information"] = supporting_information + +# Check if any Supporting Files were uploaded +try: + supporting_files +except NameError: + is_supporting_files = False +else: + supporting_files = [x for x in supporting_files if len(x) > 0] + if len(supporting_files) > 0: + is_supporting_files = True + else: + del(supporting_files) + is_supporting_files = False + + Flow_1whl6cg + Flow_18911q5 + + + + Cancel Request + + Flow_1uda7lq + + + + Flow_0z8xl4u + Flow_0l0kxuh + + + + + Flow_1wk4ctc + Flow_1b7wncj + Flow_0l0kxuh + Flow_0gs9a10 + + + + + + + + + Edit Request + + Flow_1w54t94 + + + + + + + Flow_0dgrlbd + Flow_1sx4un4 + + + + User Form is different + + + + 1 request per role - to get confirmation from JB + + + + Should be Program/Service Lead, L2 is not always the same + + + + no need for items + + + + for Hiring manager - use Typeahead + + + + User Form is TBC one more time with JB + + + + currency will be always USD? + + + + Can the amount be in crypto? + + + + + Flow_1svj7af + Flow_0idzytl + + Flow_09uui77 + + + Flow_09uui77 + Flow_1bkg437 + Flow_0epa5vy + + + + Flow_1bkg437 + Flow_1wlj1lr + Flow_1ipspcm + + + + last_approval_outcome == "nai" or last_approval_outcome == "rej" + + + + Flow_0cablju + Flow_1wlj1lr + # Determine index of last Reviewer Comment +review_comment_cnt = len(review_comment_list) +nai_index = review_comment_cnt - 1 + +# Get Last Reviewer's Greet Name +reviewer_greet_names = {k: v["Greet Name"] for d in approvers for k, v in d.items()} +last_reviewer = review_comment_list[nai_index]["Reviewer"] +last_reviewer_greet_name = reviewer_greet_names[last_reviewer] + +# Get Last Reviewer Comment +nai_reviewer_comment = {} +nai_reviewer_comment["Reviewer Role"] = last_reviewer +nai_reviewer_comment["Reviewer Comment Date"] = local_date_str +nai_reviewer_comment["Reviewer Comment Time"] = local_time_str +nai_reviewer_comment["Reviewer Comment"] = review_comment_list[nai_index]["Reviewer Comment"] + +# Set Variable to Indicate Addition Information has been Requested +is_additional_information = True + +# Reset Requester's Supporting Information +supporting_information = trvl_details["supporting_information"] + +del(local_date_str) +del(local_time_str) + + + + Flow_0epa5vy + Flow_0cablju + + + + if last_approval_outcome == "rej": + message_id = "waku_request_rejected" +elif last_approval_outcome == "app": + message_id = "waku_trvl_request_approved" +elif last_approval_outcome == "nai": + message_id = "waku_additional_info_required" +else: + message_id = "not_set" + +# Set Requestor Status Key +as_message_status_keys = [] +as_requestor_status_key = trvl_details["requestor_status_key"] +as_message_status_keys.append(as_requestor_status_key) + # Delete Unneeded Variables +del(message_id) +del(as_message_status_keys) + Sending Requestor Waku message + + Flow_1ipspcm + Flow_1h6i1mp + + + + Flow_1h6i1mp + + + + + + last_approval_outcome == "nai" + + + + + is_request_cancelled + + + + + + + last_approval_outcome == "nai" or last_approval_outcome == "rej" + + + + + + + + + + + + + Flow_1cqv4yz + Flow_0ejzyvh + + Flow_062233f + + + + + + Flow_0g2pd95 + Flow_04lznc6 + + + Flow_06m0ary + + + + Flow_04lznc6 + Flow_06m0ary + + + + Flow_07xnd9q + Flow_0m61kxf + + + Flow_0m61kxf + Flow_0g2pd95 + + + + Flow_062233f + Flow_07xnd9q + + + This should look as a tick-box + link to the job description + + + + a link should be submitted + + + + review the fields submitted + levels ect + + + + should it include the 2 next tasks? + + + + + Flow_0mi1iph + Flow_0ez35ra + + Flow_0td3lsi + + + + + Flow_0td3lsi + Flow_0g7z4ew + + + Flow_0g7z4ew + Flow_1xvdncl + + + Flow_11bn803 + + + + Flow_1xvdncl + Flow_11bn803 + + + + + Flow_0ejzyvh + Flow_14udt27 + + + + + + Flow_0ez35ra + Flow_1bg71py + Flow_1cqv4yz + + + Flow_1qq3qyg + Flow_1ew5sem + + Flow_1ya9gzv + + + Flow_07ikkpu + + + + Flow_1ya9gzv + Flow_0q3ipnb + Flow_13qd471 + + + last_approval_outcome == "rej" + + + Flow_0q3ipnb + Flow_11zpk89 + + + + Flow_13qd471 + Flow_0eh8nri + + + last_approval_outcome == "app" + + + Flow_11zpk89 + Flow_0eh8nri + Flow_0p8nnkb + + + + + + Flow_0p8nnkb + Flow_07ikkpu + + Flow_0nvm07k + + + Flow_0j9rq6r + + + + + Flow_0nvm07k + Flow_1xjpyq1 + # Set Outcome Status +if last_approval_outcome == "app": + trvl_details["status"] = "Approved" +elif last_approval_outcome == "rej": + trvl_details["status"] = "Rejected" +else: + trvl_details["status"] = "Unknown" + +# Add Approver names to Postgres +approver_name_list = list({appr["Greet Name"] for appr in approval_history}) +approver_name = ",".join(approver_name_list) +del(approver_name_list) + +# Get Process Instance +process_info = get_toplevel_process_info() +process_instance_id = process_info["process_instance_id"] +process_instance_id_str = str(process_instance_id) +del(process_info) + + + + + # Set Details Data +requester_name = trvl_details["requestor_greet_name"] + " " + trvl_details["requestor_last_name"] +trvl_data = {"request_id": process_instance_id_str, "status": trvl_details["status"], "requestor_name": requester_name, "project": trvl_details["project_name"], "category": trvl_details["category_name"], "purpose": trvl_details["purpose"], "criticality": trvl_details["criticality"], "period": trvl_details["period"], "core_contributor_id": trvl_details["core_contributor_id"], "core_contributor_name": trvl_details["core_contributor_display"], "start_date": trvl_details["start_date"], "end_date": trvl_details["end_date"], "number_of_days": trvl_details["number_of_days"], "event_type": trvl_details["event_type_name"], "event_name": trvl_details["event_name"], "event_destination": trvl_details["event_destination_display"], "departure_from": trvl_details["departure_from_display"], "return_to": trvl_details["return_to_display"], "total_amount": trvl_details["total_request_amount_usd"], "details": trvl_details["supporting_information"], "approver_name": approver_name, "visa_required": trvl_details["visa_display"] } + +# Set Call Store Procedure +call_stored_proc_schema = { + "sql": "SELECT jsoninsert('demand_request_travel', %s);", + "values": [trvl_data], +} + # Delete Unneeded Data +del(call_stored_proc_schema) +del(requester_name) +del(trvl_data) +del(resp_DetailsDump) + + + + + + + + Flow_1xjpyq1 + Flow_17vkkm7 + + + + + + # Get request_id +details_request_id_cnt = len(resp_SelectValue) +details_request_id_index = details_request_id_cnt - 1 +details_request_id = resp_SelectValue[details_request_id_index][0] +del(resp_SelectValue) + + + + + + + + + Flow_17vkkm7 + Flow_19x61kv + + + + + # Set Item Data +col_names = {"process_instance_id": "request_id", "item_name": "item_name", "item_id": "item_id", "item_url": "item_url", "sub_category_name": "sub_category", "qty": "quantity", "unit_price_num": "unit_price", "currency": "currency", "currency_type": "currency_type", "unit_price_total": "total_amount_currency", "converted_to_unit_price_total": "total_amount_usd", "converted_to_unit_price": "unit_price_usd"} +trvl_item_data_temp = [{col_names[k]: v for k, v in x.items() if k in col_names} for x in trvl_details["item"]] +trvl_item_data = [dict(item, request_id=process_instance_id_str, request_id_db=details_request_id) for item in trvl_item_data_temp] +pd_item_id = len(trvl_item_data) + 1 +trvl_item_data.append({"total_amount_usd": trvl_details["meals_cost"], "currency": "USD", "currency_type": "fiat", "item_id": pd_item_id, "item_name": "Per Diem", "request_id": process_instance_id_str, "quantity": trvl_details["number_of_days"], "sub_category": "Meals", "unit_price": 75, "total_amount_currency": trvl_details["meals_cost"], "request_id_db": details_request_id, "unit_price_usd": 75}) +del(col_names) +del(trvl_item_data_temp) + + +call_stored_proc_schema = { + "sql": "SELECT pp2_jsonarrayinsert_item(%s);", + "values": [{ "trvl_item_data": trvl_item_data }], +} + # Delete Unneeded Variables +del(call_stored_proc_schema) +del(trvl_item_data) +del(resp_ItemsDump) + + + + + + + + Flow_19x61kv + Flow_0wquwy1 + + + Flow_0wquwy1 + Flow_1wwho23 + Flow_1ky54ip + + + is_supporting_files + + + Flow_0b1bfsh + Flow_1ky54ip + Flow_0gtenff + + + + + + # Set Data Format +trvl_file_data = [{"file_string": x, "request_id": process_instance_id_str, "request_id_db": details_request_id} for x in file_links] + +# Store Proc +call_stored_proc_schema = { + "sql": "SELECT pp2_jsonarrayinsert_files(%s);", + "values": [{ "trvl_file_data": trvl_file_data }], +} + # Delete Unneeded Variables +del(call_stored_proc_schema) +del(trvl_file_data) + + + + + + + + Flow_1wwho23 + Flow_0b1bfsh + + + + + + + + Flow_0gtenff + Flow_0j9rq6r + # Delete Unneeded Variables +del(process_instance_id_str) +del(is_additional_information) +del(is_approval_rejected) +del(last_approval_outcome) +del(nai_reviewer_comment) +del(review_comment_cnt) +del(reviewer_comment) +del(reviewer_outcome) +del(no_budget_owners_proceed) +del(process_instance_id) +# del(requestor_proceed_option) +del(l1_budget_owner_bamboo_eid) +del(l2_budget_owner_bamboo_eid) +del(details_request_id) +del(details_request_id_cnt) +del(details_request_id_index) +del(requestor_greet_name) +del(n) + +if is_supporting_files: + del(resp_FilesDump) + + + + + Flow_1ew5sem + Flow_17ajueg + + + Flow_14udt27 + Flow_1qq3qyg + Flow_1bg71py + + + + + + Process Recap +--- +### Approval History + +**Approval Status**: {{ trvl_details["status"] }} + + +| Reviewer | <div style="width:90px">Date<div> | <div style="width:65px">Time<div> | Outcome | Comment | +| ---------- | ------ | ----- | --------- | ----------- | +{% for i in approval_history %} +| {{ i["Approver"] }} | {{ i["Approval Date"] }} | {{ i["Approval Time"] }} | {{ i["Outcome"] }} | {{ i["Comment"] }} | +{% endfor %} + +---------- +**Requestor**: {{ trvl_details["requestor"] }} + +| | | +| --------------: | -------------------------------- | +|**Traveller** | {{ trvl_details["core_contributor_display"] }} | +|**Purpose** | {{ trvl_details["purpose"] }} | + +| Project | Event Type | Criticality | Event Date | Event Name | Event Destination | Visa required? | +| -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | -------------------------------- | ----- | +| {{ trvl_details["project_name"] }} | {{ trvl_details["event_type_name"] }} | {{ trvl_details["criticality"] }} | {{ trvl_details["period_display"] }} | {{ trvl_details["event_name"] }} | {{ trvl_details["event_destination_display"] }} | {{ trvl_details["visa_display"] }} | + +---------- +### Itinerary + +**Departure**: {{ trvl_details["start_date_display"] }} , {{ trvl_details["departure_from_display"] }} + +**Return**: {{ trvl_details["end_date_display"] }}, {{ trvl_details["return_to_display"] }} + +**Number of days**: {{ trvl_details["number_of_days"] }} + +--- +### Items +| Item | Sub-Category | Qty | Currency | Unit Price | Total Price | USD Price | Total USD Price | +| ---- | ----------------- | :---: | :---------: | ----------: | ------------: | -----------: | -----------------: | +| Per diem | Meals | {{ trvl_details["number_of_days"] }} | USD | 75.00 | {{ trvl_details["meals_cost_display"] }} | 75 | {{ trvl_details["meals_cost_display"] }} | +{% for icnt in range(item_list_total) %} +| {% if is_item_url_list[icnt] %}<a href="{{ item_url_list[icnt] }}" target="_blank" >{{ item_name_list[icnt] }}</a>{% else %}{{ item_name_list[icnt] }}{% endif %} | {{ trvl_details["item"][icnt]["sub_category_name"] }} | {{ trvl_details["item"][icnt]["qty"] }} | {{ trvl_details["item"][icnt]["currency"] }} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_num"]) }}{% endif %} | {% if trvl_details["item"][icnt]["currency_type"] == "crypto" %}{{ "{:,.4f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% else %}{{ "{:,.2f}".format(trvl_details["item"][icnt]["unit_price_total"]) }}{% endif %} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price"]) }} | {{ "{:,.2f}".format(trvl_details["item"][icnt]["converted_to_unit_price_total"]) }} | +{% endfor %} +---------- +### Total Request: {{ "{:,.2f}".format(trvl_details["total_request_amount_usd"]) }} USD** + + +<sub> *Fiat - Exchange Rates used to calculate the "USD" amounts are under <a href="http://www.xe.com/" target="_blank" >license from Xe</a>. Please note that if you use this Xe Data, you are obliged to comply with <a href="http://www.xe.com/legal/dfs.php" target="_blank" >Xe's end terms of use</a>. <sub> + +<sub> *Crypto - Exchange Rates used to calculate the "USD" amounts are received from <a href="https://www.coingecko.com/en/api" target="_blank" >CoinGecko</a>.<sub> + +---------- +{% if is_supporting_information %} +### Supporting Information +| | | +| --------------: | -------------------------------- | +|**More details** | {{ trvl_details["supporting_information"] }} | + +---------- +{% endif %} + +{% if is_supporting_files %} + +### Attachments + +{% for link in file_links %} + {{link}} +{% endfor %} + +{% endif %} + + Flow_1ge8w7r + + + Flow_1b4y857 + Flow_1ge8w7r + + + Flow_17ajueg + Flow_1b4y857 + + + L2 BO - ? TBD + + + + How to get list of all recruiters? + + + + create a role PeopleOps (Talent), get the list of the users with this role - select a user + + + + can someone raise the request on behalf of someone else? + + + + to requestor and Head of PeopleOps partner + + + + TBC + + + + the job can be published + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +