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

Bug Fix: After testing #35

Merged
merged 14 commits into from
Nov 30, 2023
112 changes: 103 additions & 9 deletions apps/accounting_exports/models.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField
from typing import List

from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models import Count
from fyle_accounting_mappings.models import ExpenseAttribute

from apps.fyle.models import Expense
from apps.workspaces.models import BaseForeignWorkspaceModel, BaseModel, ExportSetting
from ms_business_central_api.models.fields import (
BooleanFalseField,
CustomDateTimeField,
CustomJsonField,
IntegerNullField,
StringNotNullField,
StringNullField,
CustomJsonField,
CustomDateTimeField,
StringOptionsField,
IntegerNullField,
BooleanFalseField,
TextNotNullField
TextNotNullField,
)
from apps.workspaces.models import BaseForeignWorkspaceModel, BaseModel
from apps.fyle.models import Expense

TYPE_CHOICES = (
('INVOICES', 'INVOICES'),
Expand All @@ -31,6 +34,55 @@
)


def _group_expenses(expenses: List[Expense], export_setting: ExportSetting, fund_source: str):
"""
Group expenses based on specified fields
"""

credit_card_expense_grouped_by = export_setting.credit_card_expense_grouped_by
credit_card_expense_date = export_setting.credit_card_expense_date
reimbursable_expense_grouped_by = export_setting.reimbursable_expense_grouped_by
reimbursable_expense_date = export_setting.reimbursable_expense_date

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

# Define a mapping for fund sources and their associated group fields
fund_source_mapping = {
'CCC': {
'group_by': report_grouping_fields if credit_card_expense_grouped_by == 'REPORT' else expense_grouping_fields,
'date_field': credit_card_expense_date.lower() if credit_card_expense_date != 'LAST_SPENT_AT' else None
},
'PERSONAL': {
'group_by': report_grouping_fields if reimbursable_expense_grouped_by == 'REPORT' else expense_grouping_fields,
'date_field': reimbursable_expense_date.lower() if reimbursable_expense_date != 'LAST_SPENT_AT' else None
}
}

# Update expense_group_fields based on the fund_source
fund_source_data = fund_source_mapping.get(fund_source)
group_by_field = fund_source_data.get('group_by')
date_field = fund_source_data.get('date_field')

default_fields.extend(group_by_field)

if date_field:
default_fields.append(date_field)

# Extract expense IDs from the provided expenses
expense_ids = [expense.id for expense in expenses]
# Retrieve expenses from the database
expenses = Expense.objects.filter(id__in=expense_ids).all()

# Create expense groups by grouping expenses based on specified fields
expense_groups = list(expenses.values(*default_fields).annotate(
total=Count('*'), expense_ids=ArrayAgg('id'))
)

return expense_groups


class AccountingExport(BaseForeignWorkspaceModel):
"""
Table to store accounting exports
Expand All @@ -50,6 +102,48 @@ class AccountingExport(BaseForeignWorkspaceModel):
class Meta:
db_table = 'accounting_exports'

@staticmethod
def create_accounting_export(expense_objects: List[Expense], fund_source: str, workspace_id):
"""
Group expenses by report_id and fund_source, format date fields, and create AccountingExport objects.
"""
# Retrieve the ExportSetting for the workspace
export_setting = ExportSetting.objects.get(workspace_id=workspace_id)

# Group expenses based on specified fields and fund_source
accounting_exports = _group_expenses(expense_objects, export_setting, fund_source)

fund_source_map = {
'PERSONAL': 'reimbursable',
'CCC': 'credit_card'
}

for accounting_export in accounting_exports:
# Determine the date field based on fund_source
date_field = getattr(export_setting, f"{fund_source_map.get(fund_source)}_expense_date", None)

ruuushhh marked this conversation as resolved.
Show resolved Hide resolved
# Calculate and assign 'last_spent_at' based on the chosen date field
if date_field == 'last_spent_at':
latest_expense = Expense.objects.filter(id__in=accounting_export['expense_ids']).order_by('-spent_at').first()
accounting_export['last_spent_at'] = latest_expense.spent_at if latest_expense else None

# Store expense IDs and remove unnecessary keys
expense_ids = accounting_export['expense_ids']
accounting_export[date_field] = accounting_export[date_field].strftime('%Y-%m-%dT%H:%M:%S')
accounting_export.pop('total')
accounting_export.pop('expense_ids')

# Create an AccountingExport object for the expense group
accounting_export_instance = AccountingExport.objects.create(
workspace_id=workspace_id,
fund_source=accounting_export['fund_source'],
description=accounting_export,
status='EXPORT_READY'
)

# Add related expenses to the AccountingExport object
accounting_export_instance.expenses.add(*expense_ids)


class AccountingExportSummary(BaseModel):
"""
Expand Down
2 changes: 1 addition & 1 deletion apps/business_central/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self, credentials_object: BusinessCentralCredentials, workspace_id:
refresh_token = credentials_object.refresh_token

self.connection = Dynamics(
enviroment=environment,
environment=environment,
client_id=client_id,
client_secret=client_secret,
refresh_token=refresh_token,
Expand Down
10 changes: 5 additions & 5 deletions apps/fyle/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def queue_import_reimbursable_expenses(workspace_id: int, synchronous: bool = Fa

if not synchronous:
async_task(
'apps.fyle.tasks.import_reimbursable_expenses',
workspace_id, accounting_export,
'apps.fyle.tasks.import_expenses',
workspace_id, accounting_export, 'PERSONAL_CASH_ACCOUNT', 'PERSONAL'
)
return

Expand All @@ -49,9 +49,9 @@ def queue_import_credit_card_expenses(workspace_id: int, synchronous: bool = Fal

if not synchronous:
async_task(
'apps.fyle.tasks.import_credit_card_expenses',
workspace_id, accounting_export,
'apps.fyle.tasks.import_expenses',
workspace_id, accounting_export, 'PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT', 'CCC'
)
return

import_expenses(workspace_id, accounting_export, 'PERSONAL_CASH_ACCOUNT', 'PERSONAL')
import_expenses(workspace_id, accounting_export, 'PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT', 'CCC')
19 changes: 10 additions & 9 deletions apps/fyle/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
"""
import logging
from datetime import datetime, timezone

from django.db.models import Q
from fyle_accounting_mappings.models import ExpenseAttribute
from fyle_integrations_platform_connector import PlatformConnector
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from rest_framework.views import status
from fyle_integrations_platform_connector import PlatformConnector

from fyle_accounting_mappings.models import ExpenseAttribute

from apps.fyle.models import ExpenseFilter
from apps.workspaces.models import Workspace, FyleCredential
from apps.fyle.helpers import get_expense_fields
from apps.fyle.models import ExpenseFilter
from apps.workspaces.models import FyleCredential, Workspace

logger = logging.getLogger(__name__)
logger.level = logging.INFO
Expand Down Expand Up @@ -112,14 +112,15 @@ class ExpenseFieldSerializer(serializers.Serializer):
"""
Workspace Admin Serializer
"""
expense_fields = serializers.SerializerMethodField()
field_name = serializers.CharField()
type = serializers.CharField()
is_custom = serializers.BooleanField()

def get_expense_fields(self, validated_data):
def get_expense_fields(self, workspace_id:int):
"""
Get Expense Fields
"""

workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id')
expense_fields = get_expense_fields(workspace_id=workspace_id)

return expense_fields
2 changes: 1 addition & 1 deletion apps/fyle/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ def import_expenses(workspace_id, accounting_export: AccountingExport, source_ac
)

accounting_export.status = 'COMPLETE'
accounting_export.errors = None
accounting_export.business_central_errors = None

accounting_export.save()
6 changes: 4 additions & 2 deletions apps/fyle/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
FyleFieldsSerializer,
ImportFyleAttributesSerializer,
)
from apps.workspaces.models import Workspace
from ms_business_central_api.utils import LookupFieldMixin

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -62,7 +61,10 @@ class CustomFieldView(generics.ListAPIView):
"""

serializer_class = ExpenseFieldSerializer
queryset = Workspace.objects.all()
pagination_class = None

def get_queryset(self):
return ExpenseFieldSerializer().get_expense_fields(self.kwargs["workspace_id"])


class ExportableExpenseGroupsView(generics.RetrieveAPIView):
Expand Down
19 changes: 19 additions & 0 deletions apps/users/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Tuple

from fyle_integrations_platform_connector import PlatformConnector
from fyle_rest_auth.models import AuthToken

from apps.fyle.helpers import get_cluster_domain
Expand All @@ -20,3 +21,21 @@ def get_cluster_domain_and_refresh_token(user) -> Tuple[str, str]:
cluster_domain = get_cluster_domain(refresh_token)

return cluster_domain, refresh_token


def get_user_profile(request):
"""
Get user profile
"""
refresh_token = AuthToken.objects.get(user__user_id=request.user).refresh_token
cluster_domain, _ = get_cluster_domain_and_refresh_token(request.user)

fyle_credentials = FyleCredential(
cluster_domain=cluster_domain,
refresh_token=refresh_token
)

platform = PlatformConnector(fyle_credentials)
employee_profile = platform.connection.v1beta.spender.my_profile.get()

return employee_profile
3 changes: 2 additions & 1 deletion apps/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
"""
from django.urls import path

from apps.users.views import FyleOrgsView
from apps.users.views import FyleOrgsView, UserProfileView

urlpatterns = [
path('profile/', UserProfileView.as_view(), name='user-profile'),
path('orgs/', FyleOrgsView.as_view(), name='fyle-orgs')
]
17 changes: 16 additions & 1 deletion apps/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rest_framework.response import Response

from apps.fyle.helpers import get_fyle_orgs
from apps.users.helpers import get_cluster_domain_and_refresh_token
from apps.users.helpers import get_cluster_domain_and_refresh_token, get_user_profile


class FyleOrgsView(generics.ListCreateAPIView):
Expand All @@ -24,3 +24,18 @@ def get(self, request, *args, **kwargs):
data=fyle_orgs,
status=status.HTTP_200_OK
)


class UserProfileView(generics.RetrieveAPIView):

permission_classes = [IsAuthenticated]

def get(self, request, *args, **kwargs):
"""
Get User Details
"""
employee_profile = get_user_profile(request)
return Response(
data=employee_profile,
status=status.HTTP_200_OK
)
13 changes: 6 additions & 7 deletions apps/workspaces/helpers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import logging
import base64
import requests
import json
import logging

import requests
from django.conf import settings
from future.moves.urllib.parse import urlencode
from dynamics.exceptions.dynamics_exceptions import InternalServerError, InvalidTokenError
from future.moves.urllib.parse import urlencode

from apps.workspaces.models import BusinessCentralCredentials, Workspace
from apps.business_central.utils import BusinessCentralConnector

from apps.workspaces.models import BusinessCentralCredentials, Workspace

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -119,8 +118,8 @@ def connect_business_central(authorization_code, redirect_uri, workspace_id):
workspace.business_central_company_id = connection[0]["id"]
workspace.save()

if workspace.onboarding_state == "CONNECTION":
# If workspace's onboarding state is "CONNECTION", update it to "EXPORT_SETTINGS"
if workspace.onboarding_state == "COMPANY_SELECTION":
# If workspace's onboarding state is "COMPANY_SELECTION", update it to "EXPORT_SETTINGS"
workspace.onboarding_state = "EXPORT_SETTINGS"
workspace.save()

Expand Down
21 changes: 11 additions & 10 deletions apps/workspaces/models.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
from django.db import models

from ms_business_central_api.models.fields import (
StringNotNullField,
BooleanFalseField,
BooleanTrueField,
CustomDateTimeField,
CustomJsonField,
IntegerNullField,
StringNotNullField,
StringNullField,
StringOptionsField,
TextNotNullField,
StringNullField,
BooleanTrueField,
BooleanFalseField,
IntegerNullField,
CustomJsonField
)

User = get_user_model()

ONBOARDING_STATE_CHOICES = (
('CONNECTION', 'CONNECTION'),
('COMPANY_SELECTION', 'COMPANY_SELECTION'),
('EXPORT_SETTINGS', 'EXPORT_SETTINGS'),
('IMPORT_SETTINGS', 'IMPORT_SETTINGS'),
('ADVANCED_CONFIGURATION', 'ADVANCED_CONFIGURATION'),
Expand All @@ -26,7 +27,7 @@


def get_default_onboarding_state():
return 'EXPORT_SETTINGS'
return 'CONNECTION'


class Workspace(models.Model):
Expand All @@ -37,8 +38,8 @@ class Workspace(models.Model):
name = StringNotNullField(help_text='Name of the workspace')
user = models.ManyToManyField(User, help_text='Reference to users table')
org_id = models.CharField(max_length=255, help_text='org id', unique=True)
last_synced_at = CustomDateTimeField(help_text='Datetime when expenses were pulled last')
ccc_last_synced_at = CustomDateTimeField(help_text='Datetime when ccc expenses were pulled last')
reimbursable_last_synced_at = CustomDateTimeField(help_text='Datetime when expenses were pulled last')
credit_card_last_synced_at = CustomDateTimeField(help_text='Datetime when ccc expenses were pulled last')
source_synced_at = CustomDateTimeField(help_text='Datetime when source dimensions were pulled')
destination_synced_at = CustomDateTimeField(help_text='Datetime when destination dimensions were pulled')
onboarding_state = StringOptionsField(
Expand Down
Loading
Loading