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

Dependent Fields added to Import settings #75

Merged
merged 10 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions apps/fyle/migrations/0003_alter_dependentfieldsetting_workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.1.2 on 2023-11-06 11:11

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


class Migration(migrations.Migration):

dependencies = [
('workspaces', '0003_alter_importsetting_workspace'),
('fyle', '0002_dependentfieldsetting'),
]

operations = [
migrations.AlterField(
model_name='dependentfieldsetting',
name='workspace',
field=models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, related_name='dependent_field_settings', to='workspaces.workspace'),
),
]
2 changes: 2 additions & 0 deletions apps/fyle/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
IntegerNotNullField
)
from apps.workspaces.models import BaseModel, BaseForeignWorkspaceModel
from apps.workspaces.models import Workspace


EXPENSE_FILTER_RANK = (
Expand Down Expand Up @@ -192,6 +193,7 @@ class DependentFieldSetting(BaseModel):
cost_category_field_id = StringNotNullField(help_text='Fyle Cost Category Field ID')
cost_category_placeholder = models.TextField(blank=True, null=True, help_text='Placeholder for Cost Category')
last_successful_import_at = CustomDateTimeField(null=True, help_text='Last Successful Import At')
workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model', related_name='dependent_field_settings')

class Meta:
db_table = 'dependent_field_settings'
Expand Down
6 changes: 3 additions & 3 deletions apps/fyle/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def run_pre_save_dependent_field_settings_triggers(sender, instance: DependentFi
fyle_credentials: FyleCredential = FyleCredential.objects.get(workspace_id=instance.workspace_id)
platform = PlatformConnector(fyle_credentials=fyle_credentials)

instance.project_field_id = platform.expense_fields.get_project_field_id()
instance.project_field_id = platform.dependent_fields.get_project_field_id()

cost_code = create_dependent_custom_field_in_fyle(
workspace_id=instance.workspace_id,
Expand All @@ -45,9 +45,9 @@ def run_pre_save_dependent_field_settings_triggers(sender, instance: DependentFi

cost_category = create_dependent_custom_field_in_fyle(
workspace_id=instance.workspace_id,
fyle_attribute_type=instance.category_field_name,
fyle_attribute_type=instance.cost_category_field_name,
platform=platform,
source_placeholder=instance.category_placeholder,
source_placeholder=instance.cost_category_placeholder,
parent_field_id=instance.cost_code_field_id,
)
instance.category_field_id = cost_category['data']['id']
Expand Down
45 changes: 45 additions & 0 deletions apps/mappings/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
FYLE_EXPENSE_SYSTEM_FIELDS = [
'employee id',
'organisation name',
'employee name',
'employee email',
'expense date',
'expense id',
'report id',
'employee id',
'department',
'state',
'reporter',
'report',
'purpose',
'vendor',
'category',
'category code',
'mileage distance',
'mileage unit',
'flight from city',
'flight to city',
'flight from date',
'flight to date',
'flight from class',
'flight to class',
'hotel checkin',
'hotel checkout',
'hotel location',
'hotel breakfast',
'currency',
'amount',
'foreign currency',
'foreign amount',
'tax',
'approver',
'project',
'billable',
'cost center',
'cost center code',
'approved on',
'reimbursable',
'receipts',
'paid date',
'expense created date'
]
2 changes: 1 addition & 1 deletion apps/mappings/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def new_fn(expense_attribute_instance, *args):
import_log.status = 'FAILED'

except (Sage300Credential.DoesNotExist, InvalidUserCredentials):
error['message'] = 'Invalid Token or Sage Intacct credentials does not exist workspace_id - {0}'.format(workspace_id)
error['message'] = 'Invalid Token or Sage 300 credentials does not exist workspace_id - {0}'.format(workspace_id)
error['alert'] = False
import_log.status = 'FAILED'

Expand Down
Empty file removed apps/mappings/helpers.py
Empty file.
174 changes: 173 additions & 1 deletion apps/mappings/imports/modules/expense_custom_fields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,177 @@
from datetime import datetime
from typing import List, Dict
from apps.mappings.imports.modules.base import Base
from fyle_accounting_mappings.models import (
DestinationAttribute,
ExpenseAttribute
)
from apps.mappings.exceptions import handle_import_exceptions
from apps.mappings.models import ImportLog
from fyle_integrations_platform_connector import PlatformConnector
from apps.workspaces.models import FyleCredential
from apps.mappings.constants import FYLE_EXPENSE_SYSTEM_FIELDS


class ExpenseCustomField(Base):
pass
"""
Class for ExepenseCustomField module
"""
def __init__(self, workspace_id: int, source_field: str, destination_field: str, sync_after: datetime):
super().__init__(
workspace_id=workspace_id,
source_field=source_field,
destination_field=destination_field,
platform_class_name='expense_custom_fields',
sync_after=sync_after
)

def trigger_import(self):
"""
Trigger import for ExepenseCustomField module
"""
self.check_import_log_and_start_import()

def construct_custom_field_placeholder(self, source_placeholder: str, fyle_attribute: str, existing_attribute: Dict):
"""
Construct placeholder for custom field
:param source_placeholder: Placeholder from mapping settings
:param fyle_attribute: Fyle attribute
:param existing_attribute: Existing attribute
"""
new_placeholder = None
placeholder = None

if existing_attribute:
placeholder = existing_attribute['placeholder'] if 'placeholder' in existing_attribute else None

# Here is the explanation of what's happening in the if-else ladder below
# source_field is the field that's save in mapping settings, this field user may or may not fill in the custom field form
# placeholder is the field that's saved in the detail column of destination attributes
# fyle_attribute is what we're constructing when both of these fields would not be available

if not (source_placeholder or placeholder):
# If source_placeholder and placeholder are both None, then we're creating adding a self constructed placeholder
new_placeholder = 'Select {0}'.format(fyle_attribute)
elif not source_placeholder and placeholder:
# If source_placeholder is None but placeholder is not, then we're choosing same place holder as 1 in detail section
new_placeholder = placeholder
elif source_placeholder and not placeholder:
# If source_placeholder is not None but placeholder is None, then we're choosing the placeholder as filled by user in form
new_placeholder = source_placeholder
else:
# Else, we're choosing the placeholder as filled by user in form or None
new_placeholder = source_placeholder

return new_placeholder

def construct_fyle_expense_custom_field_payload(
self,
sage300_attributes: List[DestinationAttribute],
platform: PlatformConnector,
source_placeholder: str = None
):
"""
Construct payload for expense custom fields
:param sage300_attributes: List of destination attributes
:param platform: PlatformConnector object
:param source_placeholder: Placeholder from mapping settings
"""
fyle_expense_custom_field_options = []
fyle_attribute = self.source_field

[fyle_expense_custom_field_options.append(sage300_attribute.value) for sage300_attribute in sage300_attributes]

if fyle_attribute.lower() not in FYLE_EXPENSE_SYSTEM_FIELDS:
existing_attribute = ExpenseAttribute.objects.filter(
attribute_type=fyle_attribute, workspace_id=self.workspace_id).values_list('detail', flat=True).first()

custom_field_id = None

if existing_attribute is not None:
custom_field_id = existing_attribute['custom_field_id']

fyle_attribute = fyle_attribute.replace('_', ' ').title()
placeholder = self.construct_custom_field_placeholder(source_placeholder, fyle_attribute, existing_attribute)

expense_custom_field_payload = {
'field_name': fyle_attribute,
'type': 'SELECT',
'is_enabled': True,
'is_mandatory': False,
'placeholder': placeholder,
'options': fyle_expense_custom_field_options,
'code': None
}

if custom_field_id:
expense_field = platform.expense_custom_fields.get_by_id(custom_field_id)
expense_custom_field_payload['id'] = custom_field_id
expense_custom_field_payload['is_mandatory'] = expense_field['is_mandatory']

return expense_custom_field_payload

# construct_payload_and_import_to_fyle method is overridden
def construct_payload_and_import_to_fyle(
self,
platform: PlatformConnector,
import_log: ImportLog,
source_placeholder: str = None
):
"""
Construct Payload and Import to fyle in Batches
"""
filters = self.construct_attributes_filter(self.destination_field)

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

# If there are no destination attributes, mark the import as complete
if destination_attributes_count == 0:
import_log.status = 'COMPLETE'
import_log.last_successful_run_at = datetime.now()
import_log.error_log = []
import_log.total_batches_count = 0
import_log.processed_batches_count = 0
import_log.save()
return
else:
import_log.total_batches_count = 1
import_log.save()

destination_attributes = DestinationAttribute.objects.filter(**filters)
destination_attributes_without_duplicates = self.remove_duplicate_attributes(destination_attributes)
platform_class = self.get_platform_class(platform)

fyle_payload = self.construct_fyle_expense_custom_field_payload(
destination_attributes_without_duplicates,
platform,
source_placeholder
)

self.post_to_fyle_and_sync(
fyle_payload=fyle_payload,
resource_class=platform_class,
is_last_batch=True,
import_log=import_log
)

# import_destination_attribute_to_fyle method is overridden
@handle_import_exceptions
def import_destination_attribute_to_fyle(self, import_log: ImportLog):
"""
Import destiantion_attributes field to Fyle and Auto Create Mappings
:param import_log: ImportLog object
"""

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

self.sync_destination_attributes(self.destination_field)

self.construct_payload_and_import_to_fyle(
platform=platform,
import_log=import_log
)

self.sync_expense_attributes(platform)

self.create_mappings()
12 changes: 6 additions & 6 deletions apps/mappings/imports/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@
from apps.workspaces.models import ImportSetting


def schedule_or_delete_dependent_field_tasks(import_settings: ImportSetting):
def schedule_or_delete_dependent_field_tasks(workspace_id: int):
"""
:param configuration: Workspace Configuration Instance
:return: None
"""
project_mapping = MappingSetting.objects.filter(
source_field='PROJECT',
workspace_id=import_settings.workspace_id,
workspace_id=workspace_id,
import_to_fyle=True
).first()
dependent_fields = DependentFieldSetting.objects.filter(workspace_id=import_settings.workspace_id, is_import_enabled=True).first()
dependent_fields = DependentFieldSetting.objects.filter(workspace_id=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),
args='{}'.format(workspace_id),
defaults={
'schedule_type': Schedule.MINUTES,
'minutes': 24 * 60,
Expand All @@ -32,7 +32,7 @@ def schedule_or_delete_dependent_field_tasks(import_settings: ImportSetting):
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)
args='{}'.format(workspace_id)
).delete()


Expand Down Expand Up @@ -80,4 +80,4 @@ def schedule_or_delete_fyle_import_tasks(import_settings: ImportSetting, mapping
).delete()

# Schedule or delete dependent field tasks
schedule_or_delete_dependent_field_tasks(import_settings=import_settings)
schedule_or_delete_dependent_field_tasks(import_settings.workspace_id)
2 changes: 1 addition & 1 deletion apps/mappings/imports/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def auto_import_and_map_fyle_fields(workspace_id):
chain = Chain()

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

if chain.length() > 0:
chain.run()
2 changes: 1 addition & 1 deletion apps/mappings/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def run_pre_mapping_settings_triggers(sender, instance: MappingSetting, **kwargs
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
# TODO: sync sage 300 fields before we upload custom field
try:
workspace_id = int(instance.workspace_id)
# Checking is import_log exists or not if not create one
Expand Down
2 changes: 1 addition & 1 deletion apps/mappings/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""fyle_intacct_api URL Configuration
"""fyle_sage_desktop_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/
Expand Down
6 changes: 3 additions & 3 deletions apps/sage300/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ def create(self, validated_data):

# Retrieve the workspace and Sage 300 credentials
workspace = Workspace.objects.get(pk=workspace_id)
sage_intacct_credentials = Sage300Credential.objects.get(
sage_300_credentials = Sage300Credential.objects.get(
workspace_id=workspace.id
)

if refresh_dimension:
# If 'refresh' is true, perform a full sync of dimensions
sync_dimensions(sage_intacct_credentials, workspace.id)
sync_dimensions(sage_300_credentials, workspace.id)
else:
# If 'refresh' is false, check the interval and sync dimension accordingly
check_interval_and_sync_dimension(workspace, sage_intacct_credentials)
check_interval_and_sync_dimension(workspace, sage_300_credentials)

# Update the destination_synced_at field and save the workspace
workspace.destination_synced_at = datetime.now()
Expand Down
2 changes: 1 addition & 1 deletion apps/users/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""fyle_intacct_api URL Configuration
"""fyle_sage_desktop_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/
Expand Down
Loading
Loading