Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expense creation from Slack (both form as well as DE flow) #40

Merged
merged 93 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
21ce0e8
Dynamic modal test
shreyanshss7 Jul 20, 2021
cf24ed2
Expense fields stuff
shreyanshss7 Aug 21, 2021
364cbda
dynamic expense form poc
shreyanshss7 Aug 23, 2021
13a3d6b
dynamic expense form poc
shreyanshss7 Aug 23, 2021
582b85b
Some more testing with dynamic forms
shreyanshss7 Aug 23, 2021
9c2720e
Expense form dynamic details extraction
shreyanshss7 Aug 24, 2021
7cc1164
Expense form dynamic details extraction
shreyanshss7 Aug 24, 2021
4e683c6
Fetching expense id from parent message
shreyanshss7 Aug 27, 2021
f354d8c
Merge branch 'master' into dynamic-modal
shreyanshss7 Aug 30, 2021
4a65b10
tested multi select
shreyanshss7 Aug 31, 2021
b5a3fdb
Separated out custom fields in expense
shreyanshss7 Aug 31, 2021
b41e4c6
Tested boolean field custom fields
shreyanshss7 Sep 1, 2021
5120785
Minor stuff
shreyanshss7 Sep 2, 2021
ffd1632
Added class based handler
shreyanshss7 Sep 2, 2021
fe0f9f2
showing custom fields in expense details
shreyanshss7 Sep 2, 2021
e56b92b
External select of categories
shreyanshss7 Sep 7, 2021
1908812
Minor
shreyanshss7 Sep 13, 2021
24cc4b0
Merged master
shreyanshss7 Sep 16, 2021
0d58d6b
Added initial expense form interaction
shreyanshss7 Sep 17, 2021
0e49ed7
Handling project & category selection in background and rendering of …
shreyanshss7 Sep 21, 2021
8f96243
Added loading below input elements
shreyanshss7 Sep 21, 2021
ccce79e
Common connection for fyle expense
shreyanshss7 Sep 21, 2021
649602e
Added cost center and billable and following expense form order
shreyanshss7 Sep 22, 2021
b27f03a
Moved current ui logic to message for reusability
shreyanshss7 Sep 22, 2021
ab8f255
Added validation checks
shreyanshss7 Sep 22, 2021
98d061c
Projects, cost centers & category as dynamic selects
shreyanshss7 Sep 23, 2021
80918f0
Moved out mandatory field check to FyleExpense and call api only if d…
shreyanshss7 Sep 23, 2021
7936ca3
Support for non home currency and dynamic input value change
shreyanshss7 Sep 24, 2021
3825b41
Moved currency and amount entered select to async
shreyanshss7 Sep 24, 2021
1bb7743
Using private metadata
shreyanshss7 Sep 27, 2021
4ffd61d
Using expense form v2 - much cleaner
shreyanshss7 Sep 27, 2021
cf770ca
Removed older expense form code and added support for custom fields r…
shreyanshss7 Sep 27, 2021
d4dc7af
Reduced a lot of api calls and made things faster
shreyanshss7 Sep 28, 2021
f83d993
Added support for non default custom fields as well
shreyanshss7 Sep 29, 2021
52e3aba
Edit expense support
shreyanshss7 Oct 1, 2021
4670a95
Add to report dialog and edit expense
shreyanshss7 Oct 4, 2021
d8a1f6d
Added to report dialog final
shreyanshss7 Oct 4, 2021
e2242c5
Submit report dialog and update expense changes
shreyanshss7 Oct 5, 2021
4b73f82
Minor fix for currency select
shreyanshss7 Oct 5, 2021
b3cdf83
Added report submitted message
shreyanshss7 Oct 6, 2021
49c146e
Slack interactives inheritance fix and add to report submission
shreyanshss7 Oct 6, 2021
b56ad20
Added proper CTAs and their dialogs and upgraded fyle sdk
shreyanshss7 Oct 7, 2021
4421687
Removed unnecessary project check and added some code comments
shreyanshss7 Oct 11, 2021
2cc8e9f
Handling add to report submission
shreyanshss7 Oct 11, 2021
5b263cd
Remove file upload stuff
shreyanshss7 Oct 11, 2021
68dc646
Expense form func indentation
shreyanshss7 Oct 11, 2021
471ed95
Separated out expense form logic in a single function
shreyanshss7 Oct 13, 2021
af08654
Removed a lot of redundant code
shreyanshss7 Oct 13, 2021
97d6d3b
Added return statements directly
shreyanshss7 Oct 14, 2021
2e1dca2
Handling report details in background
shreyanshss7 Oct 19, 2021
ab02b34
indent fix
shreyanshss7 Oct 22, 2021
9ef0a72
Minor changes
shreyanshss7 Oct 26, 2021
4054ac5
Removing custom fields when project is selected
shreyanshss7 Oct 26, 2021
9c5a619
added separate func for project and cost center availability
shreyanshss7 Oct 26, 2021
a7f9994
resolved PR comments
shreyanshss7 Nov 3, 2021
7864c49
resolved PR comments
shreyanshss7 Nov 3, 2021
1adf87f
Resolved PR comments
shreyanshss7 Nov 8, 2021
1504253
Changed fyler -> spender
shreyanshss7 Dec 7, 2021
f2d0922
Merge branch 'master' into expense
shreyanshss7 Dec 7, 2021
e706fb5
Using cache for storing form metadata
shreyanshss7 Dec 7, 2021
c74ee0d
Merged exchange rate api
shreyanshss7 Dec 8, 2021
185bb8b
Support for location
shreyanshss7 Dec 15, 2021
fd9c504
Added merchant as select field
shreyanshss7 Feb 18, 2022
366db3a
Changes projects and categories behaviour acc to API changes
shreyanshs7 Mar 28, 2022
2a1cd0a
Merged master and resolved conflicts
shreyanshs7 Apr 26, 2022
a4c7331
Merge branch 'master' into expense
shreyanshs7 May 6, 2022
de89246
Minor
shreyanshs7 May 6, 2022
ce2cf17
Merged master and resolved conflicts
shreyanshs7 May 6, 2022
dd77030
Upsert Expense Changes via Platform (#80)
shreyanshss7 May 10, 2022
e765ab5
Resolved pylint issues
shreyanshs7 May 10, 2022
707821b
Reverted qcluster workers to 4
shreyanshs7 May 11, 2022
9aa3847
Added fix for locations and travel classes
shreyanshs7 May 11, 2022
acf597d
removed hard coded expense id
shreyanshs7 May 11, 2022
b597f81
Location field changes
shreyanshs7 May 12, 2022
1c7728e
Minor date field none check
shreyanshs7 May 12, 2022
4720f57
Minor unlink account fix
shreyanshs7 May 12, 2022
fdc67a4
removed print statements
shreyanshs7 May 13, 2022
f4e0ce4
removed unsued import - pylint issue
shreyanshs7 May 13, 2022
33fe4ea
Merge branch 'master' into expense
shreyanshs7 May 13, 2022
7781535
Catching invalid usage while creating expense
shreyanshs7 May 13, 2022
0718df7
removed unused import
shreyanshs7 May 13, 2022
3807abf
Expense Creation: DE Flow (#84)
shreyanshss7 May 17, 2022
8bc1065
Minor none check
shreyanshs7 May 19, 2022
e1709f4
Minor none check
shreyanshs7 May 19, 2022
b844c48
Minor changes
Jun 2, 2022
de03b29
Modify expense-form slash command for Slack
Jun 7, 2022
da7325e
Undo previous commit
Jun 9, 2022
84c1390
Fix edit expense form modal bug (#85)
Jun 14, 2022
c51bf76
Remove unused commented code
Jun 14, 2022
28a8fbc
Fix pylint errors
Jun 14, 2022
c01dc3d
Fix pylint errors
Jun 14, 2022
b3ea4cd
Add trackers for expense creation from slack (#87)
Jun 15, 2022
184c3d8
Fix pylint error
Jun 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ confidence=
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=print-statement,
consider-using-f-string,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
Expand Down
238 changes: 236 additions & 2 deletions fyle_slack_app/fyle/expenses/views.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,252 @@
from typing import Dict
from typing import Dict, List

import datetime

from django.core.cache import cache

from fyle.platform.platform import Platform

from fyle_slack_app.fyle.utils import get_fyle_sdk_connection
from fyle_slack_app.models.users import User
from fyle_slack_app.fyle import utils as fyle_utils
from fyle_slack_app.libs import assertions, http


# pylint: disable=too-many-public-methods
class FyleExpense:

connection: Platform = None

def __init__(self, user: User) -> None:
self.connection = get_fyle_sdk_connection(user.fyle_refresh_token)


def get_expense_fields(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.expense_fields.list(query_params=query_params)


def get_default_expense_fields(self) -> Dict:
default_expense_fields_query_params = {
'offset': 0,
'limit': '50',
'order': 'created_at.desc',
'column_name': 'in.(purpose, txn_dt, vendor_id, cost_center_id, project_id)',
'is_enabled': 'eq.{}'.format(True),
'is_custom': 'eq.{}'.format(False)
}

return self.get_expense_fields(default_expense_fields_query_params)


def get_custom_fields_by_category_id(self, category_id: str) -> Dict:
custom_fields_query_params = {
'offset': 0,
'limit': '50',
'order': 'created_at.desc',
'column_name': 'not_in.(purpose, txn_dt, spent_at, merchant, vendor_id, cost_center_id)',
'is_enabled': 'eq.{}'.format(True),
'category_ids': 'cs.[{}]'.format(int(category_id))
}

return self.get_expense_fields(custom_fields_query_params)


def get_categories(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.categories.list(query_params=query_params)


def get_projects(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.projects.list(query_params=query_params)

def get_merchants(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.merchants.list(query_params=query_params)

def get_cost_centers(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.cost_centers.list(query_params=query_params)


def get_expenses(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.expenses.list(query_params=query_params)


def get_reports(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.reports.list(query_params=query_params)


def get_employees(self, query_params: Dict) -> Dict:
return self.connection.v1beta.spender.employees.list(query_params=query_params)


def get_places_autocomplete(self, query: str) -> Dict:
return self.connection.v1beta.common.places_autocomplete.list(q=query)


def get_place_by_place_id(self, place_id: str) -> Dict:
return self.connection.v1beta.common.places.get_by_id(place_id)


def get_exchange_rate(self, from_currency: str, to_currency: str) -> Dict:
current_date = datetime.datetime.today().strftime('%Y-%m-%d')
exchange_rate = self.connection.v1beta.common.currencies_exchange_rate.get(
from_currency, to_currency, current_date
)
return exchange_rate['data']['exchange_rate']


def check_project_availability(self) -> bool:
projects_query_params = {
'offset': 0,
'limit': '1',
'order': 'created_at.desc',
'is_enabled': 'eq.{}'.format(True)
}

projects = self.get_projects(projects_query_params)

is_project_available = True if projects['count'] > 0 else False

return is_project_available


def check_cost_center_availability(self) -> bool:
cost_centers_query_params = {
'offset': 0,
'limit': '1',
'order': 'created_at.desc',
'is_enabled': 'eq.{}'.format(True)
}

cost_centers = self.get_cost_centers(cost_centers_query_params)

is_cost_center_available = True if cost_centers['count'] > 0 else False

return is_cost_center_available


def upsert_expense(self, expense_payload: Dict, refresh_token: str) -> Dict:
access_token = fyle_utils.get_fyle_access_token(refresh_token)
cluster_domain = fyle_utils.get_cluster_domain(refresh_token)

url = '{}/platform/v1/spender/expenses'.format(cluster_domain)
headers = {
'content-type': 'application/json',
'Authorization': 'Bearer {}'.format(access_token)
}

expense_payload = {
'data': expense_payload
}

response = http.post(url, json=expense_payload, headers=headers)
assertions.assert_valid(response.status_code == 200, 'Error creating expense')
return response.json()['data']


@staticmethod
def get_currencies():
return ['ADP','AED','AFA','ALL','AMD','ANG','AOA','ARS','ATS','AUD','AWG','AZM','BAM','BBD','BDT','BEF','BGL','BGN','BHD','BIF','BMD','BND','BOB','BOV','BRL','BSD','BTN','BWP','BYB','BZD','CAD','CDF','CHF','CLF','CLP','CNY','COP','CRC','CUP','CVE','CYP','CZK','DEM','DJF','DKK','DOP','DZD','ECS','ECV','EEK','EGP','ERN','ESP','ETB','EUR','FIM','FJD','FKP','FRF','GBP','GEL','GHC','GIP','GMD','GNF','GRD','GTQ','GWP','GYD','HKD','HNL','HRK','HTG','HUF','IDE','IDR','IEP','ILS','INR','IQD','IRR','ISK','ITL','JMD','JOD','JPY','KES','KGS','KHR','KMF','KPW','KRW','KWD','KYD','KZT','LAK','LBP','LKR','LRD','LSL','LTL','LUF','LVL','LYD','MAD','MDL','MGF','MKD','MMK','MNT','MOP','MRO','MTL','MUR','MVR','MWK','MXN','MXV','MYR','MZM','NAD','NGN','NIO','NLG','NOK','NPR','NZD','OMR','PAB','PEN','PGK','PHP','PKR','PLN','PTE','PYG','QAR','ROL','RUB','RUR','RWF','RYR','SAR','SBD','SCR','SDP','SEK','SGD','SHP','SIT','SKK','SLL','SOS','SRG','STD','SVC','SYP','SZL','THB','TJR','TMM','TND','TOP','TPE','TRL','TTD','TWD','TZS','UAH','UGX','USD','USN','USS','UYU','UZS','VEB','VND','VUV','WST','XAF','XCD','XDR','XEU','XOF','XPF','YER','YUN','ZAR','ZMK','ZRN','ZWD']


@staticmethod
def get_expense_fields_mandatory_mapping(expense_fields: List[Dict]) -> Dict:
mandatory_fields_mapping = {
'purpose': False,
'txn_dt': False,
'vendor_id': False,
'project_id': False,
'cost_center_id': False
}

for field in expense_fields['data']:
if field['column_name'] in mandatory_fields_mapping:
mandatory_fields_mapping[field['column_name']] = field['is_mandatory']

return mandatory_fields_mapping


@staticmethod
def get_expense_form_details(user: User, view_id: str) -> Dict:

fyle_expense = FyleExpense(user)

fyle_profile = fyle_utils.get_fyle_profile(user.fyle_refresh_token)

home_currency = fyle_profile['org']['currency']

default_expense_fields = fyle_expense.get_default_expense_fields()

mandatory_fields_mapping = fyle_expense.get_expense_fields_mandatory_mapping(default_expense_fields)

is_project_available = fyle_expense.check_project_availability()
is_cost_center_available = fyle_expense.check_cost_center_availability()

# Create a expense fields render property and set them optional in the form
fields_render_property = {
'project': {
'is_project_available': is_project_available,
'is_mandatory': mandatory_fields_mapping['project_id']
},
'cost_center': {
'is_cost_center_available': is_cost_center_available,
'is_mandatory': mandatory_fields_mapping['cost_center_id']
},
'purpose': mandatory_fields_mapping['purpose'],
'transaction_date': mandatory_fields_mapping['txn_dt'],
'vendor': mandatory_fields_mapping['vendor_id']
}

additional_currency_details = {
'home_currency': home_currency
}

add_to_report = 'existing_report'

expense_form_details = {
'fields_render_property': fields_render_property,
'additional_currency_details': additional_currency_details,
'add_to_report': add_to_report
}

cache_key = '{}.form_metadata'.format(view_id)
cache.set(cache_key, expense_form_details, 3600)

return expense_form_details


@staticmethod
def get_current_expense_form_details(slack_payload: Dict) -> Dict:

cache_key = '{}.form_metadata'.format(slack_payload['view']['id'])
form_metadata = cache.get(cache_key)

fields_render_property = form_metadata['fields_render_property']

additional_currency_details = form_metadata.get('additional_currency_details')

add_to_report = form_metadata.get('add_to_report')

current_ui_blocks = slack_payload['view']['blocks']

project = form_metadata.get('project')

custom_field_blocks = []

for block in current_ui_blocks:
if 'custom_field' in block['block_id'] or 'additional_field' in block['block_id']:
custom_field_blocks.append(block)

if len(custom_field_blocks) == 0:
custom_field_blocks = None

current_form_details = {
'fields_render_property': fields_render_property,
'selected_project': project,
'additional_currency_details': additional_currency_details,
'add_to_report': add_to_report,
'custom_fields': custom_field_blocks
}
return current_form_details


def get_expense_by_id(self, expense_id: str) -> Dict:
query_params = {
'id': 'eq.{}'.format(expense_id),
Expand Down
33 changes: 27 additions & 6 deletions fyle_slack_app/slack/commands/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

from fyle.platform import exceptions

from fyle_slack_app import tracking
from fyle_slack_app.libs import utils, assertions, logger
from fyle_slack_app.fyle.utils import get_fyle_oauth_url, get_fyle_profile
from fyle_slack_app.models import User, NotificationPreference
from fyle_slack_app.slack.ui.common_messages import IN_PROGRESS_MESSAGE
from fyle_slack_app.slack.ui.dashboard import messages as dashboard_messages
from fyle_slack_app.slack.ui.notifications import preference_messages as notification_preference_messages
from fyle_slack_app.slack.ui.expenses import messages as expense_messages
from fyle_slack_app.slack import utils as slack_utils
from fyle_slack_app import tracking


logger = logger.get_logger(__name__)
Expand All @@ -26,10 +27,11 @@ class SlackCommandHandler:
def _initialize_command_handlers(self):
self._command_handlers = {
'fyle_unlink_account': self.handle_fyle_unlink_account,
'fyle_notification_preferences': self.handle_fyle_notification_preferences
'fyle_notification_preferences': self.handle_fyle_notification_preferences,
'expense_form': self.handle_expense_form
}

def handle_invalid_command(self, user_id: str, team_id: str, user_dm_channel_id: str) -> JsonResponse:
def handle_invalid_command(self, user_id: str, team_id: str, user_dm_channel_id: str, trigger_id: str) -> JsonResponse:
slack_client = slack_utils.get_slack_client(team_id)

slack_client.chat_postMessage(
Expand All @@ -39,14 +41,14 @@ def handle_invalid_command(self, user_id: str, team_id: str, user_dm_channel_id:
return JsonResponse({}, status=200)


def handle_slack_command(self, command: str, user_id: str, team_id: str, user_dm_channel_id: str) -> Callable:
def handle_slack_command(self, command: str, user_id: str, team_id: str, user_dm_channel_id: str, trigger_id: str) -> Callable:

# Initialize slack command handlers
self._initialize_command_handlers()

handler = self._command_handlers.get(command, self.handle_invalid_command)

return handler(user_id, team_id, user_dm_channel_id)
return handler(user_id, team_id, user_dm_channel_id, trigger_id)


def handle_fyle_unlink_account(self, user_id: str, team_id: str, user_dm_channel_id: str) -> JsonResponse:
Expand Down Expand Up @@ -86,7 +88,7 @@ def update_home_tab_with_pre_auth_message(self, user_id: str, team_id: str) -> N
slack_client.views_publish(user_id=user_id, view=pre_auth_message_view)


def handle_fyle_notification_preferences(self, user_id: str, team_id: str, user_dm_channel_id: str) -> JsonResponse:
def handle_fyle_notification_preferences(self, user_id: str, team_id: str, user_dm_channel_id: str, trigger_id: str) -> JsonResponse:
user = utils.get_or_none(User, slack_user_id=user_id)
assertions.assert_found(user, 'Slack user not found')

Expand All @@ -110,6 +112,25 @@ def handle_fyle_notification_preferences(self, user_id: str, team_id: str, user_
return JsonResponse({}, status=200)


def handle_expense_form(self, user_id: str, team_id: str, user_dm_channel_id: str, trigger_id: str):
user = utils.get_or_none(User, slack_user_id=user_id)

slack_client = slack_utils.get_slack_client(team_id)

loading_modal = expense_messages.expense_form_loading_modal(title='Create Expense', loading_message='Loading the best expense form :zap:')

response = slack_client.views_open(user=user_id, view=loading_modal, trigger_id=trigger_id)

async_task(
'fyle_slack_app.slack.commands.tasks.open_expense_form',
user,
team_id,
response['view']['id']
)

return JsonResponse({}, status=200)


def track_fyle_account_unlinked(self, user: User) -> None:
event_data = {
'asset': 'SLACK_APP',
Expand Down
15 changes: 15 additions & 0 deletions fyle_slack_app/slack/commands/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from fyle_slack_app.models import User, UserSubscriptionDetail
from fyle_slack_app.models.user_subscription_details import SubscriptionType
from fyle_slack_app.slack.commands.handlers import SlackCommandHandler
from fyle_slack_app.fyle.expenses.views import FyleExpense
from fyle_slack_app.slack.ui.expenses import messages as expense_messages


logger = logger.get_logger(__name__)
Expand Down Expand Up @@ -98,3 +100,16 @@ def fyle_unlink_account(user_id: str, team_id: str, user_dm_channel_id: str, mes
blocks=message_block,
ts=message_ts
)


def open_expense_form(user: User, team_id: str, view_id: str) -> None:

slack_client = slack_utils.get_slack_client(team_id)

expense_form_details = FyleExpense.get_expense_form_details(user, view_id)

expense_form = expense_messages.expense_dialog_form(
**expense_form_details
)

slack_client.views_update(view=expense_form, view_id=view_id)
3 changes: 2 additions & 1 deletion fyle_slack_app/slack/commands/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ def post(self, request: HttpRequest, command: str) -> HttpResponse:
team_id = request.POST['team_id']
user_id = request.POST['user_id']
user_dm_channel_id = request.POST['channel_id']
trigger_id = request.POST['trigger_id']

self.handle_slack_command(command, user_id, team_id, user_dm_channel_id)
self.handle_slack_command(command, user_id, team_id, user_dm_channel_id, trigger_id)

# Empty "" HttpResponse beacause for slash commands slack return the response as message to user
return HttpResponse("", status=200)
2 changes: 0 additions & 2 deletions fyle_slack_app/slack/events/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
from slack_sdk import WebClient

from django.conf import settings

from fyle_slack_app.fyle.expenses.views import FyleExpense

from fyle_slack_app.fyle.utils import get_fyle_oauth_url
from fyle_slack_app.libs import utils, assertions, logger
from fyle_slack_app.models import Team, User, UserSubscriptionDetail
from fyle_slack_app.models.user_subscription_details import SubscriptionType
from fyle_slack_app.fyle import utils as fyle_utils

from fyle_slack_app.slack.interactives.block_action_handlers import BlockActionHandler
from fyle_slack_app.slack import utils as slack_utils
from fyle_slack_app.slack.ui.authorization import messages
Expand Down
Loading