Skip to content

Commit

Permalink
Advanced Settings V2 API (#436)
Browse files Browse the repository at this point in the history
* onboarding state implementation

* tests migrations

* added onboarding state

* changed comment

* added subsidiary state to onboarding state

* changed script to add subsidiary state and fixed some bug

* bug fix

* state change on connection and subsidiary change

* map employees v2 api

* map_employees typos

* bug fix

* export setting changes

* export settings V2 api

* added test for export settings api

* resolved comments

* import settings v2 api

* test added for import settings v2 api

* advanced settings v2 api

* advanced settings v2 api with test case

* resolved comments

* First schedule should be triggered after interval hours and Handle Admin GET in a safer way (#437)

* First schedule should be triggered after interval hours

* Handle Admin GET in a safer way

* Making reimbursable expense object nullable and checking edge cases  (#438)

* Making reimbursbale expense object nullable and checking edge cases for the same

* comment resolved

* resolving comments

* all comment resolved

* added code in test for the changes

* added test code for the changes

---------

Co-authored-by: Ashutosh619-sudo <[email protected]>

---------

Co-authored-by: Ashutosh619-sudo <[email protected]>

* changes as per comments

---------

Co-authored-by: Ashutosh619-sudo <[email protected]>
Co-authored-by: Nilesh Pant <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2023
1 parent d26f22e commit 64d953d
Show file tree
Hide file tree
Showing 17 changed files with 366 additions and 23 deletions.
4 changes: 2 additions & 2 deletions apps/fyle/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ def update_import_card_credits_flag(corporate_credit_card_expenses_object: str,
expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id)
import_card_credits = None

if (corporate_credit_card_expenses_object == 'EXPENSE REPORT' or reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY']) and not expense_group_settings.import_card_credits:
if (corporate_credit_card_expenses_object == 'EXPENSE REPORT' or (reimbursable_expenses_object and reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and not expense_group_settings.import_card_credits:
import_card_credits = True
elif (corporate_credit_card_expenses_object != 'EXPENSE REPORT' and reimbursable_expenses_object not in ['EXPENSE REPORT', 'JOURNAL ENTRY']) and expense_group_settings.import_card_credits:
elif (corporate_credit_card_expenses_object != 'EXPENSE REPORT' and (reimbursable_expenses_object and reimbursable_expenses_object not in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and expense_group_settings.import_card_credits:
import_card_credits = False

if corporate_credit_card_expenses_object == 'CREDIT CARD CHARGE':
Expand Down
2 changes: 1 addition & 1 deletion apps/fyle/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense

if total_amount < 0:
reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses))
elif configuration.reimbursable_expenses_object != 'JOURNAL ENTRY':
elif configuration.reimbursable_expenses_object != 'JOURNAL ENTRY':
reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses))

expense_groups = _group_expenses(reimbursable_expenses, reimbursable_expense_group_fields, workspace_id)
Expand Down
10 changes: 5 additions & 5 deletions apps/mappings/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ def sync_expense_categories_and_accounts(configuration: Configuration, netsuite_
:netsuite_connection: NetSuite Connection Object
:return: None
"""
if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' or configuration.corporate_credit_card_expenses_object == 'EXPENSE REPORT':
if (configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object == 'EXPENSE REPORT') or configuration.corporate_credit_card_expenses_object == 'EXPENSE REPORT':
netsuite_connection.sync_expense_categories()

if configuration.reimbursable_expenses_object in ('BILL', 'JOURNAL ENTRY') or \
if (configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object in ('BILL', 'JOURNAL ENTRY')) or \
configuration.corporate_credit_card_expenses_object in ('BILL', 'JOURNAL ENTRY', 'CREDIT CARD CHARGE'):
netsuite_connection.sync_accounts()

Expand Down Expand Up @@ -226,8 +226,8 @@ def upload_categories_to_fyle(workspace_id: int, configuration: Configuration, p
if configuration.import_categories:
netsuite_accounts = DestinationAttribute.objects.filter(
workspace_id=workspace_id,
attribute_type='EXPENSE_CATEGORY' if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' else 'ACCOUNT',
display_name='Expense Category' if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' else 'Account'
attribute_type='EXPENSE_CATEGORY' if configuration.reimbursable_expenses_object == 'EXPENSE_REPORT' or configuration.corporate_credit_card_expenses_object == 'EXPENSE_REPORT' else 'ACCOUNT',
display_name='Expense Category' if configuration.reimbursable_expenses_object == 'EXPENSE_REPORT' or configuration.corporate_credit_card_expenses_object == 'EXPENSE_REPORT' else 'Account'
)
if netsuite_accounts:
netsuite_attributes = netsuite_accounts
Expand Down Expand Up @@ -479,7 +479,7 @@ def auto_create_category_mappings(workspace_id):
reimbursable_expenses_object = configuration.reimbursable_expenses_object
corporate_credit_card_expenses_object = configuration.corporate_credit_card_expenses_object

if reimbursable_expenses_object == 'EXPENSE REPORT':
if configuration.reimbursable_expenses_object == 'EXPENSE_REPORT' or configuration.corporate_credit_card_expenses_object == 'EXPENSE_REPORT':
reimbursable_destination_type = 'EXPENSE_CATEGORY'
else:
reimbursable_destination_type = 'ACCOUNT'
Expand Down
Empty file.
159 changes: 159 additions & 0 deletions apps/workspaces/apis/advanced_settings/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from rest_framework import serializers

from apps.workspaces.models import Configuration, Workspace, WorkspaceSchedule
from apps.mappings.models import GeneralMapping
from apps.workspaces.apis.advanced_settings.triggers import AdvancedConfigurationsTriggers


class ReadWriteSerializerMethodField(serializers.SerializerMethodField):
"""
Serializer Method Field to Read and Write from values
Inherits serializers.SerializerMethodField
"""

def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
kwargs['source'] = '*'
super(serializers.SerializerMethodField, self).__init__(**kwargs)

def to_internal_value(self, data):
return {self.field_name: data}


class ConfigurationSerializer(serializers.ModelSerializer):

class Meta:
model = Configuration
fields = [
'change_accounting_period',
'sync_fyle_to_netsuite_payments',
'sync_netsuite_to_fyle_payments',
'auto_create_destination_entity',
'memo_structure'
]


class GeneralMappingsSerializer(serializers.ModelSerializer):

netsuite_location = ReadWriteSerializerMethodField()
netsuite_location_level = ReadWriteSerializerMethodField()
department_level = ReadWriteSerializerMethodField()
use_employee_location = ReadWriteSerializerMethodField()
use_employee_department = ReadWriteSerializerMethodField()
use_employee_class = ReadWriteSerializerMethodField()

class Meta:
model = GeneralMapping
fields = [
'netsuite_location',
'netsuite_location_level',
'department_level',
'use_employee_location',
'use_employee_department',
'use_employee_class'
]


def get_netsuite_location(self, instance: GeneralMapping):
return {
'name': instance.location_name,
'id': instance.location_id
}

def get_netsuite_location_level(self, instance: GeneralMapping):
return instance.location_level

def get_department_level(self, instance: GeneralMapping):
return instance.department_level

def get_use_employee_location(self, instance: GeneralMapping):
return instance.use_employee_location

def get_use_employee_department(self, instance: GeneralMapping):
return instance.use_employee_department

def get_use_employee_class(self, instance: GeneralMapping):
return instance.use_employee_class

class WorkspaceSchedulesSerializer(serializers.ModelSerializer):
emails_selected = serializers.ListField(allow_null=True, required=False)

class Meta:
model = WorkspaceSchedule
fields = [
'enabled',
'interval_hours',
'additional_email_options',
'emails_selected'
]

class AdvancedSettingsSerializer(serializers.ModelSerializer):
"""
Serializer for the Advanced Configurations Form/API
"""
configuration = ConfigurationSerializer()
general_mappings = GeneralMappingsSerializer()
workspace_schedules = WorkspaceSchedulesSerializer()
workspace_id = serializers.SerializerMethodField()

class Meta:
model = Workspace
fields = [
'configuration',
'general_mappings',
'workspace_schedules',
'workspace_id'
]
read_only_fields = ['workspace_id']


def get_workspace_id(self, instance):
return instance.id

def update(self, instance, validated):
configurations = validated.pop('configuration')
general_mappings = validated.pop('general_mappings')
workspace_schedules = validated.pop('workspace_schedules')

configuration_instance, _ = Configuration.objects.update_or_create(
workspace=instance,
defaults={
'sync_fyle_to_netsuite_payments': configurations.get('sync_fyle_to_netsuite_payments'),
'sync_netsuite_to_fyle_payments': configurations.get('sync_netsuite_to_fyle_payments'),
'auto_create_destination_entity': configurations.get('auto_create_destination_entity'),
'change_accounting_period': configurations.get('change_accounting_period'),
'memo_structure': configurations.get('memo_structure')
}
)

GeneralMapping.objects.update_or_create(
workspace=instance,
defaults={
'netsuite_location': general_mappings.get('netsuite_location'),
'netsuite_location_level': general_mappings.get('netsuite_location_level'),
'department_level': general_mappings.get('department_level'),
'use_employee_location': general_mappings.get('use_employee_location'),
'use_employee_department': general_mappings.get('use_employee_department'),
'use_employee_class': general_mappings.get('use_employee_class')
}
)

AdvancedConfigurationsTriggers.run_post_configurations_triggers(instance.id, workspace_schedule=workspace_schedules, configuration=configuration_instance)

if instance.onboarding_state == 'ADVANCED_CONFIGURATION':
instance.onboarding_state = 'COMPLETE'
instance.save()

return instance

def validate(self, data):
if not data.get('configuration'):
raise serializers.ValidationError('Configurations are required')

if not data.get('general_mappings'):
raise serializers.ValidationError('General mappings are required')

if not data.get('workspace_schedules'):
raise serializers.ValidationError('Workspace Schedules are required')

return data
24 changes: 24 additions & 0 deletions apps/workspaces/apis/advanced_settings/triggers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from apps.netsuite.helpers import schedule_payment_sync
from apps.workspaces.models import Configuration, WorkspaceSchedule
from apps.workspaces.tasks import schedule_sync


class AdvancedConfigurationsTriggers:
"""
Class containing all triggers for advanced_configurations
"""
@staticmethod
def run_post_configurations_triggers(workspace_id, workspace_schedule: WorkspaceSchedule, configuration: Configuration):
"""
Run workspace general settings triggers
"""

schedule_sync(
workspace_id=workspace_id,
schedule_enabled=workspace_schedule.get('enabled'),
hours=workspace_schedule.get('interval_hours'),
email_added=workspace_schedule.get('additional_email_options'),
emails_selected=workspace_schedule.get('emails_selected')
)

schedule_payment_sync(configuration=configuration)
11 changes: 11 additions & 0 deletions apps/workspaces/apis/advanced_settings/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from rest_framework import generics

from apps.workspaces.models import Workspace
from apps.workspaces.apis.advanced_settings.serializers import AdvancedSettingsSerializer


class AdvancedSettingsView(generics.RetrieveUpdateAPIView):
serializer_class = AdvancedSettingsSerializer

def get_object(self):
return Workspace.objects.filter(id=self.kwargs['workspace_id']).first()
2 changes: 2 additions & 0 deletions apps/workspaces/apis/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.urls import path
from apps.workspaces.apis.advanced_settings.views import AdvancedSettingsView

from apps.workspaces.apis.map_employees.views import MapEmployeesView
from apps.workspaces.apis.export_settings.views import ExportSettingsView
Expand All @@ -10,4 +11,5 @@
path('<int:workspace_id>/map_employees/', MapEmployeesView.as_view(), name='map-employees'),
path('<int:workspace_id>/export_settings/', ExportSettingsView.as_view(), name='export-settings'),
path('<int:workspace_id>/import_settings/', ImportSettingsView.as_view(), name='import-settings'),
path('<int:workspace_id>/advanced_settings/', AdvancedSettingsView.as_view(), name='advanced-settings'),
]
19 changes: 19 additions & 0 deletions apps/workspaces/migrations/0036_auto_20231027_0709.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.1.14 on 2023-10-27 07:09

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('workspaces', '0035_auto_20231019_1025'),
]

operations = [
migrations.AlterField(
model_name='workspaceschedule',
name='workspace',
field=models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, related_name='workspace_schedules', to='workspaces.workspace'),
),
]
2 changes: 1 addition & 1 deletion apps/workspaces/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class WorkspaceSchedule(models.Model):
Workspace Schedule
"""
id = models.AutoField(primary_key=True, help_text='Unique Id to identify a schedule')
workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model')
workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model', related_name='workspace_schedules')
enabled = models.BooleanField(default=False)
start_datetime = models.DateTimeField(help_text='Datetime for start of schedule', null=True)
interval_hours = models.IntegerField(null=True)
Expand Down
3 changes: 2 additions & 1 deletion apps/workspaces/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ def schedule_sync(workspace_id: int, schedule_enabled: bool, hours: int, email_a
if email_added:
ws_schedule.additional_email_options.append(email_added)

next_run = datetime.now() + timedelta(hours=hours)

schedule, _ = Schedule.objects.update_or_create(
func='apps.workspaces.tasks.run_sync_schedule',
args='{}'.format(workspace_id),
defaults={
'schedule_type': Schedule.MINUTES,
'minutes': hours * 60,
'next_run': datetime.now()
'next_run': next_run
}
)

Expand Down
13 changes: 7 additions & 6 deletions apps/workspaces/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,16 +459,17 @@ def get(self, request, *args, **kwargs):
users = workspace.user.all()
for user in users:
admin = User.objects.get(user_id=user)
name = ExpenseAttribute.objects.get(
employee = ExpenseAttribute.objects.filter(
value=admin.email,
workspace_id=kwargs['workspace_id'],
attribute_type='EMPLOYEE'
).detail['full_name']
).first()

admin_email.append({
'name': name,
'email': admin.email
})
if employee:
admin_email.append({
'name': employee.detail['full_name'],
'email': admin.email
})

return Response(
data=admin_email,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_mappings/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ def test_upload_categories_to_fyle(mocker, db):

configuration = Configuration.objects.filter(workspace_id=49).first()
configuration.reimbursable_expenses_object = 'EXPENSE REPORT'
configuration.employee_field_mapping = 'EMPLOYEE'
configuration.corporate_credit_card_expenses_object = 'BILL'
configuration.import_categories = True
configuration.save()
Expand All @@ -290,7 +291,7 @@ def test_upload_categories_to_fyle(mocker, db):

assert expense_category_count == 36

assert len(netsuite_attributes) == 36
assert len(netsuite_attributes) == 137

count_of_accounts = DestinationAttribute.objects.filter(
attribute_type='ACCOUNT', workspace_id=49).count()
Expand Down Expand Up @@ -336,7 +337,7 @@ def test_filter_unmapped_destinations(db, mocker):
netsuite_attributes = upload_categories_to_fyle(workspace_id=1,configuration=configuration, platform=platform)

destination_attributes = filter_unmapped_destinations('EXPENSE_CATEGORY', netsuite_attributes)
assert len(destination_attributes) == 33
assert len(destination_attributes) == 124


def test_schedule_creation(db):
Expand Down Expand Up @@ -387,7 +388,7 @@ def test_auto_create_category_mappings(db, mocker):
assert response == None

mappings_count = CategoryMapping.objects.filter(workspace_id=1).count()
assert mappings_count == 34
assert mappings_count == 122

configuration = Configuration.objects.get(workspace_id=49)
configuration.reimbursable_expenses_object = 'BILL'
Expand All @@ -403,7 +404,6 @@ def test_auto_create_category_mappings(db, mocker):
destination_attribute.save()

configuration = Configuration.objects.filter(workspace_id=49).first()

configuration.import_categories = True
configuration.save()

Expand Down
Empty file.
Loading

0 comments on commit 64d953d

Please sign in to comment.