Skip to content

Commit

Permalink
add support for scheduling
Browse files Browse the repository at this point in the history
  • Loading branch information
NileshPant1999 committed Nov 3, 2023
1 parent 956b860 commit 84f437a
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 8 deletions.
7 changes: 5 additions & 2 deletions apps/fyle/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from django.dispatch import receiver

from fyle_integrations_platform_connector import PlatformConnector
from apps.workspaces.models import FyleCredential
from apps.workspaces.models import FyleCredential, ImportSetting
from apps.fyle.models import DependentFieldSetting
from apps.sage300.dependent_fields import create_dependent_custom_field_in_fyle
from apps.mappings.imports.schedules import schedule_or_delete_fyle_import_tasks


logger = logging.getLogger(__name__)
logger.level = logging.INFO
Expand Down Expand Up @@ -58,4 +60,5 @@ def run_post_save_dependent_field_settings_triggers(sender, instance: DependentF
:param instance: Row instance of Sender Class
:return: None
"""
pass
import_settings = ImportSetting.objects.filter(workspace_id=instance.workspace_id).first()
schedule_or_delete_fyle_import_tasks(import_settings)
2 changes: 0 additions & 2 deletions apps/fyle/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ class DependentFieldSettingView(generics.CreateAPIView, generics.RetrieveUpdateA
"""
Dependent Field view
"""
authentication_classes = []
permission_classes = []
serializer_class = DependentFieldSettingSerializer
lookup_field = 'workspace_id'
queryset = DependentFieldSetting.objects.all()
4 changes: 4 additions & 0 deletions apps/mappings/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
class MappingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.mappings"

def ready(self):
super(MappingsConfig, self).ready()
import apps.mappings.signals # noqa
10 changes: 7 additions & 3 deletions apps/mappings/imports/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ def construct_payload_and_import_to_fyle(

destination_attributes_count = DestinationAttribute.objects.filter(**filters).count()

is_auto_sync_status_allowed = self.get_auto_sync_permission()

# If there are no destination attributes, mark the import as complete
if destination_attributes_count == 0:
import_log.status = 'COMPLETE'
Expand All @@ -209,7 +211,8 @@ def construct_payload_and_import_to_fyle(

for paginated_destination_attributes, is_last_batch in destination_attributes_generator:
fyle_payload = self.setup_fyle_payload_creation(
paginated_destination_attributes=paginated_destination_attributes
paginated_destination_attributes=paginated_destination_attributes,
is_auto_sync_status_allowed=is_auto_sync_status_allowed
)

self.post_to_fyle_and_sync(
Expand All @@ -236,7 +239,8 @@ def get_destination_attributes_generator(self, destination_attributes_count: int

def setup_fyle_payload_creation(
self,
paginated_destination_attributes: List[DestinationAttribute]
paginated_destination_attributes: List[DestinationAttribute],
is_auto_sync_status_allowed: bool
):
"""
Setup Fyle Payload Creation
Expand All @@ -247,7 +251,7 @@ def setup_fyle_payload_creation(
paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes]
existing_expense_attributes_map = self.get_existing_fyle_attributes(paginated_destination_attribute_values)

return self.construct_fyle_payload(paginated_destination_attributes, existing_expense_attributes_map)
return self.construct_fyle_payload(paginated_destination_attributes, existing_expense_attributes_map, is_auto_sync_status_allowed)

def get_existing_fyle_attributes(self, paginated_destination_attribute_values: List[str]):
"""
Expand Down
43 changes: 43 additions & 0 deletions apps/mappings/imports/queues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django_q.tasks import Chain
from fyle_accounting_mappings.models import MappingSetting
from apps.workspaces.models import ImportSetting


def chain_import_fields_to_fyle(workspace_id):
"""
Chain import fields to Fyle
:param workspace_id: Workspace Id
"""
mapping_settings = MappingSetting.objects.filter(workspace_id=workspace_id, import_to_fyle=True)
custom_field_mapping_settings = MappingSetting.objects.filter(workspace_id=workspace_id, is_custom=True, import_to_fyle=True)
import_settings = ImportSetting.objects.get(workspace_id=workspace_id)
chain = Chain()

if import_settings.import_categories:
chain.append(
'apps.mappings.imports.tasks.trigger_import_via_schedule',
workspace_id,
'ACCOUNT',
'CATEGORY'
)

for mapping_setting in mapping_settings:
if mapping_setting.source_field in ['PROJECT', 'COST_CENTER']:
chain.append(
'apps.mappings.imports.tasks.trigger_import_via_schedule',
workspace_id,
mapping_setting.destination_field,
mapping_setting.source_field
)

for custom_fields_mapping_setting in custom_field_mapping_settings:
chain.append(
'apps.mappings.imports.tasks.trigger_import_via_schedule',
workspace_id,
custom_fields_mapping_setting.destination_field,
custom_fields_mapping_setting.source_field,
True
)

if chain.length() > 0:
chain.run()
83 changes: 83 additions & 0 deletions apps/mappings/imports/schedules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from datetime import datetime
from django_q.models import Schedule
from fyle_accounting_mappings.models import MappingSetting

from apps.fyle.models import DependentFieldSetting
from apps.workspaces.models import ImportSetting


def schedule_or_delete_dependent_field_tasks(import_settings: ImportSetting):
"""
:param configuration: Workspace Configuration Instance
:return: None
"""
project_mapping = MappingSetting.objects.filter(
source_field='PROJECT',
workspace_id=import_settings.workspace_id,
import_to_fyle=True
).first()
dependent_fields = DependentFieldSetting.objects.filter(workspace_id=import_settings.workspace_id, is_import_enabled=True).first()

if project_mapping and dependent_fields:
start_datetime = datetime.now()
Schedule.objects.update_or_create(
func='apps.mappings.tasks.auto_import_and_map_fyle_fields',
args='{}'.format(import_settings.workspace_id),
defaults={
'schedule_type': Schedule.MINUTES,
'minutes': 24 * 60,
'next_run': start_datetime
}
)
elif not (project_mapping and dependent_fields):
Schedule.objects.filter(
func='apps.mappings.tasks.auto_import_and_map_fyle_fields',
args='{}'.format(import_settings.workspace_id)
).delete()


def schedule_or_delete_fyle_import_tasks(import_settings: ImportSetting, mapping_setting_instance: MappingSetting = None):
"""
Schedule or delete Fyle import tasks based on the import settingss.
:param import_settingss: Workspace ImportSetting Instance
:param instance: Mapping Setting Instance
:return: None
"""
task_to_be_scheduled = None
# Check if there is a task to be scheduled
if mapping_setting_instance and mapping_setting_instance.import_to_fyle:
task_to_be_scheduled = mapping_setting_instance

if task_to_be_scheduled or import_settings.import_categories:
Schedule.objects.update_or_create(
func='apps.mappings.imports.queues.chain_import_fields_to_fyle',
args='{}'.format(import_settings.workspace_id),
defaults={
'schedule_type': Schedule.MINUTES,
'minutes': 24 * 60,
'next_run': datetime.now()
}
)
return

import_fields_count = MappingSetting.objects.filter(
import_to_fyle=True,
workspace_id=import_settings.workspace_id,
source_field__in=['CATEGORY', 'PROJECT', 'COST_CENTER']
).count()

custom_field_import_fields_count = MappingSetting.objects.filter(
import_to_fyle=True,
workspace_id=import_settings.workspace_id,
is_custom=True
).count()

# If the import fields count is 0, delete the schedule
if import_fields_count == 0 and custom_field_import_fields_count == 0:
Schedule.objects.filter(
func='apps.mappings.imports.queues.chain_import_fields_to_fyle',
args='{}'.format(import_settings.workspace_id)
).delete()

# Schedule or delete dependent field tasks
schedule_or_delete_dependent_field_tasks(import_settings=import_settings)
25 changes: 25 additions & 0 deletions apps/mappings/imports/tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django_q.tasks import Chain

from fyle_accounting_mappings.models import MappingSetting

from apps.mappings.models import ImportLog
from apps.mappings.imports.modules.categories import Category
from apps.mappings.imports.modules.projects import Project
from apps.mappings.imports.modules.expense_custom_fields import ExpenseCustomField
from apps.fyle.models import DependentFieldSetting


SOURCE_FIELD_CLASS_MAP = {
Expand All @@ -27,3 +32,23 @@ def trigger_import_via_schedule(workspace_id: int, destination_field: str, sourc
module_class = SOURCE_FIELD_CLASS_MAP[source_field]
item = module_class(workspace_id, destination_field, sync_after)
item.trigger_import()


def auto_import_and_map_fyle_fields(workspace_id):
"""
Auto import and map fyle fields
"""
project_mapping = MappingSetting.objects.filter(
source_field='PROJECT',
workspace_id=workspace_id,
import_to_fyle=True
).first()
dependent_fields = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first()

chain = Chain()

if project_mapping and dependent_fields:
chain.append('apps.sage_intacct.dependent_fields.import_dependent_fields_to_fyle', workspace_id)

if chain.length() > 0:
chain.run()
115 changes: 115 additions & 0 deletions apps/mappings/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import logging
from datetime import timedelta, datetime, timezone
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

from rest_framework.exceptions import ValidationError
from fyle_accounting_mappings.models import MappingSetting
from fyle_integrations_platform_connector import PlatformConnector
from fyle.platform.exceptions import WrongParamsError

from apps.mappings.imports.schedules import schedule_or_delete_fyle_import_tasks
from apps.workspaces.models import ImportSetting
from apps.mappings.models import ImportLog
from apps.workspaces.models import FyleCredential
from apps.mappings.imports.modules.expense_custom_fields import ExpenseCustomField


logger = logging.getLogger(__name__)


@receiver(post_save, sender=MappingSetting)
def run_post_mapping_settings_triggers(sender, instance: MappingSetting, **kwargs):
"""
:param sender: Sender Class
:param instance: Row instance of Sender Class
:return: None
"""
import_settings = ImportSetting.objects.filter(workspace_id=instance.workspace_id).first()

if instance.source_field == 'PROJECT':
schedule_or_delete_fyle_import_tasks(import_settings, instance)

if instance.source_field == 'COST_CENTER':
schedule_or_delete_fyle_import_tasks(import_settings, instance)

if instance.is_custom:
schedule_or_delete_fyle_import_tasks(import_settings, instance)


@receiver(pre_save, sender=MappingSetting)
def run_pre_mapping_settings_triggers(sender, instance: MappingSetting, **kwargs):
"""
:param sender: Sender Class
:param instance: Row instance of Sender Class
:return: None
"""
default_attributes = ['EMPLOYEE', 'CATEGORY', 'PROJECT', 'COST_CENTER']

instance.source_field = instance.source_field.upper().replace(' ', '_')

if instance.source_field not in default_attributes and instance.import_to_fyle:
# TODO: sync intacct fields before we upload custom field
try:
workspace_id = int(instance.workspace_id)
# Checking is import_log exists or not if not create one
import_log, is_created = ImportLog.objects.get_or_create(
workspace_id=workspace_id,
attribute_type=instance.source_field,
defaults={
'status': 'IN_PROGRESS'
}
)

last_successful_run_at = None
if import_log and not is_created:
last_successful_run_at = import_log.last_successful_run_at if import_log.last_successful_run_at else None
time_difference = datetime.now() - timedelta(minutes=32)
offset_aware_time_difference = time_difference.replace(tzinfo=timezone.utc)

# if the import_log is present and the last_successful_run_at is less than 30mins then we need to update it
# so that the schedule can run
if last_successful_run_at and offset_aware_time_difference\
and (offset_aware_time_difference < last_successful_run_at):
import_log.last_successful_run_at = offset_aware_time_difference
last_successful_run_at = offset_aware_time_difference
import_log.save()

# Creating the expense_custom_field object with the correct last_successful_run_at value
expense_custom_field = ExpenseCustomField(
workspace_id=workspace_id,
source_field=instance.source_field,
destination_field=instance.destination_field,
sync_after=last_successful_run_at
)

fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id)
platform = PlatformConnector(fyle_credentials=fyle_credentials)

# setting the import_log status to IN_PROGRESS
import_log.status = 'IN_PROGRESS'
import_log.save()

expense_custom_field.construct_payload_and_import_to_fyle(platform, import_log)
expense_custom_field.sync_expense_attributes(platform)

# NOTE: We are not setting the import_log status to COMPLETE
# since the post_save trigger will run the import again in async manner

except WrongParamsError as error:
logger.error(
'Error while creating %s workspace_id - %s in Fyle %s %s',
instance.source_field, instance.workspace_id, error.message, {'error': error.response}
)
if error.response and 'message' in error.response:
raise ValidationError({
'message': error.response['message'],
'field_name': instance.source_field
})

# setting the import_log.last_successful_run_at to -30mins for the post_save_trigger
import_log = ImportLog.objects.filter(workspace_id=workspace_id, attribute_type=instance.source_field).first()
if import_log.last_successful_run_at:
last_successful_run_at = import_log.last_successful_run_at - timedelta(minutes=30)
import_log.last_successful_run_at = last_successful_run_at
import_log.save()
20 changes: 20 additions & 0 deletions apps/mappings/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""fyle_intacct_api URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import path, include

urlpatterns = [
path('', include('fyle_accounting_mappings.urls'))
]
2 changes: 1 addition & 1 deletion apps/sage300/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from apps.workspaces.models import Workspace, Sage300Credential
from apps.sage300.helpers import sync_dimensions, check_interval_and_sync_dimension


logger = logging.getLogger(__name__)
logger.level = logging.INFO

Expand Down Expand Up @@ -78,7 +79,6 @@ class Sage300FieldSerializer(serializers.Serializer):
display_name = serializers.CharField()

def format_sage300_fields(self, workspace_id):

attribute_types = [
"VENDOR",
"ACCOUNT",
Expand Down
Loading

0 comments on commit 84f437a

Please sign in to comment.