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

Disable sage fields #183

Merged
merged 20 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion apps/accounting_exports/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _group_expenses(expenses: List[Expense], export_setting: ExportSetting, fund
reimbursable_expense_date = export_setting.reimbursable_expense_date

default_fields = ['employee_email', 'fund_source']
report_grouping_fields = ['report_id', 'claim_number']
report_grouping_fields = ['report_id', 'claim_number', 'corporate_card_id']
expense_grouping_fields = ['expense_id', 'expense_number']

# Define a mapping for fund sources and their associated group fields
Expand Down
14 changes: 1 addition & 13 deletions apps/fyle/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
"""
import logging

from django.db.models.signals import post_save, pre_save
from django.db.models.signals import pre_save
from django.dispatch import receiver

from fyle_integrations_platform_connector import PlatformConnector
from apps.workspaces.models import FyleCredential
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_dependent_field_tasks


logger = logging.getLogger(__name__)
logger.level = logging.INFO
Expand Down Expand Up @@ -51,13 +49,3 @@ def run_pre_save_dependent_field_settings_triggers(sender, instance: DependentFi
parent_field_id=instance.cost_code_field_id,
)
instance.cost_category_field_id = cost_category['data']['id']


@receiver(post_save, sender=DependentFieldSetting)
def run_post_save_dependent_field_settings_triggers(sender, instance: DependentFieldSetting, **kwargs):
"""
:param sender: Sender Class
:param instance: Row instance of Sender Class
:return: None
"""
schedule_or_delete_dependent_field_tasks(instance.workspace_id)
10 changes: 10 additions & 0 deletions apps/mappings/imports/queues.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django_q.tasks import Chain
from fyle_accounting_mappings.models import MappingSetting
from apps.workspaces.models import ImportSetting
from apps.fyle.models import DependentFieldSetting


def chain_import_fields_to_fyle(workspace_id):
Expand All @@ -11,6 +12,9 @@ def chain_import_fields_to_fyle(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)
dependent_field_settings = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first()
project_mapping = MappingSetting.objects.filter(workspace_id=workspace_id, source_field='PROJECT', import_to_fyle=True).first()

chain = Chain()

if import_settings.import_categories:
Expand Down Expand Up @@ -47,5 +51,11 @@ def chain_import_fields_to_fyle(workspace_id):
True
)

if project_mapping and dependent_field_settings:
chain.append(
'apps.mappings.imports.tasks.auto_import_and_map_fyle_fields',
workspace_id
)

if chain.length() > 0:
chain.run()
35 changes: 0 additions & 35 deletions apps/mappings/imports/schedules.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
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(workspace_id: int):
"""
:param configuration: Workspace Configuration Instance
:return: None
"""
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()

if project_mapping and dependent_fields:
start_datetime = datetime.now()
Schedule.objects.update_or_create(
func='apps.mappings.imports.tasks.auto_import_and_map_fyle_fields',
args='{}'.format(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.imports.tasks.auto_import_and_map_fyle_fields',
args='{}'.format(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.
Expand Down Expand Up @@ -78,6 +46,3 @@ def schedule_or_delete_fyle_import_tasks(import_settings: ImportSetting, mapping
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.workspace_id)
25 changes: 13 additions & 12 deletions apps/mappings/imports/tasks.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import logging
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.cost_centers import CostCenter
from apps.mappings.imports.modules.merchants import Merchant
from apps.mappings.imports.modules.expense_custom_fields import ExpenseCustomField
from apps.fyle.models import DependentFieldSetting

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

SOURCE_FIELD_CLASS_MAP = {
'CATEGORY': Category,
Expand Down Expand Up @@ -41,20 +42,20 @@ def auto_import_and_map_fyle_fields(workspace_id):
"""
Auto import and map fyle fields
"""
project_mapping = MappingSetting.objects.filter(
source_field='PROJECT',
import_log = ImportLog.objects.filter(
workspace_id=workspace_id,
import_to_fyle=True
attribute_type = 'PROJECT'
).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.mappings.tasks.sync_sage300_attributes', 'JOB', workspace_id)
chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CODE', workspace_id)
chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CATEGORY', workspace_id)
chain.append('apps.sage300.dependent_fields.import_dependent_fields_to_fyle', workspace_id)
chain.append('apps.mappings.tasks.sync_sage300_attributes', 'JOB', workspace_id)
chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CODE', workspace_id)
chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CATEGORY', workspace_id)
chain.append('apps.sage300.dependent_fields.import_dependent_fields_to_fyle', workspace_id)

if import_log and import_log.status != 'COMPLETE':
logger.error(f"Project Import is in {import_log.status} state in WORKSPACE_ID: {workspace_id} with error {str(import_log.error_log)}")

if chain.length() > 0:
chain.run()
5 changes: 2 additions & 3 deletions apps/sage300/dependent_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ def create_dependent_custom_field_in_fyle(workspace_id: int, fyle_attribute_type
return platform.expense_custom_fields.post(expense_custom_field_payload)


def post_dependent_cost_code(dependent_field_setting: DependentFieldSetting, platform: PlatformConnector, filters: Dict) -> List[str]:

def post_dependent_cost_code(dependent_field_setting: DependentFieldSetting, platform: PlatformConnector, filters: Dict, is_enabled: bool = True) -> List[str]:
projects = CostCategory.objects.filter(**filters).values('job_name').annotate(cost_codes=ArrayAgg('cost_code_name', distinct=True))
projects_from_categories = [project['job_name'] for project in projects]
posted_cost_codes = []
Expand All @@ -91,7 +90,7 @@ def post_dependent_cost_code(dependent_field_setting: DependentFieldSetting, pla
'parent_expense_field_value': project['job_name'],
'expense_field_id': dependent_field_setting.cost_code_field_id,
'expense_field_value': cost_code,
'is_enabled': True
'is_enabled': is_enabled
})
cost_code_names.append(cost_code)

Expand Down
29 changes: 14 additions & 15 deletions apps/sage300/exports/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.db import models
from django.db.models import Sum
from fyle_accounting_mappings.models import ExpenseAttribute, Mapping, MappingSetting, EmployeeMapping, DestinationAttribute
from fyle_accounting_mappings.models import ExpenseAttribute, Mapping, MappingSetting, EmployeeMapping

from apps.accounting_exports.models import AccountingExport
from apps.fyle.models import DependentFieldSetting, Expense
Expand Down Expand Up @@ -60,7 +60,6 @@ def get_expense_purpose(workspace_id, lineitem: Expense, category: str, advance_
def get_vendor_id(accounting_export: AccountingExport):
# Retrieve export settings for the given workspace
export_settings = ExportSetting.objects.get(workspace_id=accounting_export.workspace_id)

# Extract the description from the accounting export
description = accounting_export.description

Expand All @@ -81,22 +80,22 @@ def get_vendor_id(accounting_export: AccountingExport):
# Check if the fund source is 'CCC'
elif accounting_export.fund_source == 'CCC':
# Retrieve the vendor from the first expense
expense_vendor = accounting_export.expenses.first().vendor
vendor_id = None
corporate_card_id = accounting_export.expenses.first().corporate_card_id

# Query DestinationAttribute for the vendor with case-insensitive search
if expense_vendor:
vendor = DestinationAttribute.objects.filter(
if corporate_card_id:
vendor_mapping = Mapping.objects.filter(
workspace_id=accounting_export.workspace_id,
value__icontains=expense_vendor,
attribute_type='VENDOR'
).values_list('destination_id', flat=True).first()
if not vendor:
vendor = export_settings.default_vendor_id
else:
vendor = export_settings.default_vendor_id
source_type='CORPORATE_CARD',
destination_type='VENDOR',
source__source_id=corporate_card_id
).first()

# Update vendor_id with the retrieved vendor or default to export settings
vendor_id = vendor
if vendor_mapping:
vendor_id = vendor_mapping.destination.destination_id

if not vendor_id:
vendor_id = export_settings.default_vendor_id

# Return the determined vendor_id
return vendor_id
Expand Down
102 changes: 100 additions & 2 deletions apps/sage300/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
from datetime import datetime, timezone
import logging

from typing import Dict
from django.utils.module_loading import import_string

from apps.workspaces.models import Workspace, Sage300Credential
from apps.workspaces.models import Workspace, Sage300Credential, FyleCredential
from apps.mappings.models import Version

from fyle_accounting_mappings.models import ExpenseAttribute
from fyle_integrations_platform_connector import PlatformConnector
from apps.sage300.models import CostCategory
from apps.fyle.models import DependentFieldSetting
from apps.sage300.dependent_fields import post_dependent_cost_code

logger = logging.getLogger(__name__)
logger.level = logging.INFO
Expand Down Expand Up @@ -62,3 +67,96 @@ def sync_dimensions(sage300_credential: Sage300Credential, workspace_id: int) ->
except Exception as exception:
# Log any exceptions that occur during synchronization
logger.info(exception)


def disable_projects(workspace_id: int, projects_to_disable: Dict):
Hrishabh17 marked this conversation as resolved.
Show resolved Hide resolved
"""
Disable projects in Fyle when the projects are updated in Sage 300.
This is a callback function that is triggered from accounting_mappings.
projects_to_disable object format:
{
'destination_id': {
'value': 'old_project_name',
'updated_value': 'new_project_name'
}
}

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

filters = {
'workspace_id': workspace_id,
'attribute_type': 'PROJECT',
'value__in': [projects_map['value'] for projects_map in projects_to_disable.values()]
}

# Expense attribute value map is as follows: {old_project_name: destination_id}
expense_attribute_value_map = {v['value']: k for k, v in projects_to_disable.items()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do better naming here


expense_attributes = ExpenseAttribute.objects.filter(**filters)

bulk_payload = []
for expense_attribute in expense_attributes:
code = expense_attribute_value_map.get(expense_attribute.value, None)
if code:
payload = {
'name': expense_attribute.value,
'code': code,
'description': 'Sage 300 Project - {0}, Id - {1}'.format(
expense_attribute.value,
code
),
'is_enabled': False,
'id': expense_attribute.source_id
}
else:
logger.error(f"Project with value {expense_attribute.value} not found | WORKSPACE_ID: {workspace_id}")

bulk_payload.append(payload)

sync_after = datetime.now(timezone.utc)

if bulk_payload:
logger.info(f"Disabling Projects in Fyle | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_payload)}")
platform.projects.post_bulk(bulk_payload)
platform.projects.sync(sync_after=sync_after)
else:
logger.info(f"No Projects to Disable in Fyle | WORKSPACE_ID: {workspace_id}")

update_and_disable_cost_code(workspace_id, projects_to_disable, platform)
Hrishabh17 marked this conversation as resolved.
Show resolved Hide resolved


def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, platform: PlatformConnector):
"""
Update the job_name in CostCategory and disable the old cost code in Fyle
"""
dependent_field_setting = DependentFieldSetting.objects.filter(workspace_id=workspace_id).first()
Hrishabh17 marked this conversation as resolved.
Show resolved Hide resolved

if dependent_field_setting:
filters = {
'job_id__in':list(cost_codes_to_disable.keys()),
'workspace_id': workspace_id
}

# This call will disable the cost codes in Fyle that has old project name
posted_cost_codes = post_dependent_cost_code(dependent_field_setting, platform, filters, is_enabled=False)

logger.info(f"Disabling Cost Codes in Fyle | WORKSPACE_ID: {workspace_id} | COUNT: {len(posted_cost_codes)}")
Hrishabh17 marked this conversation as resolved.
Show resolved Hide resolved

# here we are updating the CostCategory with the new project name
bulk_update_payload = []
for destination_id, value in cost_codes_to_disable.items():
cost_categories = CostCategory.objects.filter(
workspace_id=workspace_id,
job_id=destination_id
).exclude(job_name=value['updated_value'])

for cost_category in cost_categories:
cost_category.job_name = value['updated_value']
cost_category.updated_at = datetime.now(timezone.utc)
bulk_update_payload.append(cost_category)

if bulk_update_payload:
logger.info(f"Updating Cost Categories | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_update_payload)}")
CostCategory.objects.bulk_update(bulk_update_payload, ['job_name', 'updated_at'], batch_size=50)
14 changes: 12 additions & 2 deletions apps/sage300/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,18 @@ def _sync_data(self, data_gen, attribute_type, display_name, workspace_id, field
if destination_attr:
destination_attributes.append(destination_attr)

DestinationAttribute.bulk_create_or_update_destination_attributes(
destination_attributes, attribute_type, workspace_id, True)
if attribute_type == 'JOB':
project_disable_callback_path = 'apps.sage300.helpers.disable_projects'
DestinationAttribute.bulk_create_or_update_destination_attributes(
destination_attributes,
attribute_type,
workspace_id,
True,
project_disable_callback_path=project_disable_callback_path
)
else:
DestinationAttribute.bulk_create_or_update_destination_attributes(
destination_attributes, attribute_type, workspace_id, True)
else:
destination_attributes = []
for item in data_gen:
Expand Down
11 changes: 11 additions & 0 deletions apps/workspaces/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ def create(self, validated_data):
# Update workspace onboarding state
workspace = export_settings.workspace

if export_settings.credit_card_expense_export_type == 'PURCHASE_INVOICE':
MappingSetting.objects.update_or_create(
workspace_id = workspace_id,
defaults={
'source_field':'CORPORATE_CARD',
'destination_field':'VENDOR',
'import_to_fyle': False,
'is_custom': False
}
)

if workspace.onboarding_state == 'EXPORT_SETTINGS':
workspace.onboarding_state = 'IMPORT_SETTINGS'
workspace.save()
Expand Down
Loading
Loading