Skip to content

Commit

Permalink
Merge branch 'master' into dependent-fields-import-settings
Browse files Browse the repository at this point in the history
  • Loading branch information
ruuushhh committed Nov 16, 2023
2 parents 08dfd5c + ffe3f25 commit 5b1935d
Show file tree
Hide file tree
Showing 32 changed files with 961 additions and 91 deletions.
97 changes: 96 additions & 1 deletion apps/accounting_exports/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import List
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Count

from fyle_accounting_mappings.models import ExpenseAttribute

Expand All @@ -13,9 +16,10 @@
StringOptionsField,
IntegerNullField
)
from apps.workspaces.models import BaseForeignWorkspaceModel, BaseModel
from apps.workspaces.models import BaseForeignWorkspaceModel, BaseModel, ExportSetting
from apps.fyle.models import Expense


TYPE_CHOICES = (
('INVOICES', 'INVOICES'),
('DIRECT_COST', 'DIRECT_COST'),
Expand All @@ -31,6 +35,56 @@
)


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, fund_source])

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 +104,47 @@ 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)

# 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.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,
)

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


class Error(BaseForeignWorkspaceModel):
"""
Expand Down
37 changes: 37 additions & 0 deletions apps/fyle/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging
import traceback
from functools import wraps

from fyle.platform.exceptions import NoPrivilegeError

from apps.workspaces.models import FyleCredential

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


def handle_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except FyleCredential.DoesNotExist:
logger.info('Fyle credentials not found %s', args[1]) # args[1] is workspace_id
args[2].detail = {'message': 'Fyle credentials do not exist in workspace'}
args[2].status = 'FAILED'
args[2].save()

except NoPrivilegeError:
logger.info('Invalid Fyle Credentials / Admin is disabled')
args[2].detail = {'message': 'Invalid Fyle Credentials / Admin is disabled'}
args[2].status = 'FAILED'
args[2].save()

except Exception:
error = traceback.format_exc()
args[2].detail = {'error': error}
args[2].status = 'FATAL'
args[2].save()
logger.exception('Something unexpected happened workspace_id: %s %s', args[1], args[2].detail)

return wrapper
42 changes: 32 additions & 10 deletions apps/fyle/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.1.2 on 2023-10-27 09:30
# Generated by Django 4.1.2 on 2023-11-07 11:32

import django.contrib.postgres.fields
import django.core.validators
Expand Down Expand Up @@ -27,7 +27,7 @@ class Migration(migrations.Migration):
('values', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), help_text='Values for the operator', null=True, size=None)),
('rank', sage_desktop_api.models.fields.IntegerOptionsField(choices=[(1, 1), (2, 2)], default='', help_text='Rank for the filter', null=True)),
('join_by', sage_desktop_api.models.fields.StringOptionsField(choices=[('AND', 'AND'), ('OR', 'OR')], default='', help_text='Used to join the filter (AND/OR)', max_length=3, null=True)),
('is_custom', sage_desktop_api.models.fields.BooleanFalseField(default=True, help_text='Custom Field or not')),
('is_custom', sage_desktop_api.models.fields.BooleanFalseField(default=False, help_text='Custom Field or not')),
('custom_field_type', sage_desktop_api.models.fields.StringOptionsField(choices=[('SELECT', 'SELECT'), ('NUMBER', 'NUMBER'), ('TEXT', 'TEXT')], default='', help_text='Custom field type', max_length=255, null=True)),
('workspace', models.ForeignKey(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')),
],
Expand All @@ -50,19 +50,19 @@ class Migration(migrations.Migration):
('org_id', sage_desktop_api.models.fields.StringNullField(help_text='Organization ID', max_length=255, null=True)),
('expense_number', sage_desktop_api.models.fields.StringNotNullField(help_text='Expense Number', max_length=255)),
('claim_number', sage_desktop_api.models.fields.StringNotNullField(help_text='Claim Number', max_length=255)),
('amount', models.FloatField(help_text='Home Amount')),
('amount', sage_desktop_api.models.fields.FloatNullField(help_text='Home Amount', null=True)),
('currency', sage_desktop_api.models.fields.StringNotNullField(help_text='Home Currency', max_length=5)),
('foreign_amount', models.FloatField(help_text='Foreign Amount', null=True)),
('foreign_currency', sage_desktop_api.models.fields.StringNotNullField(help_text='Foreign Currency', max_length=5)),
('foreign_amount', sage_desktop_api.models.fields.FloatNullField(help_text='Foreign Amount', null=True)),
('foreign_currency', sage_desktop_api.models.fields.StringNullField(help_text='Foreign Currency', max_length=5, null=True)),
('settlement_id', sage_desktop_api.models.fields.StringNullField(help_text='Settlement ID', max_length=255, null=True)),
('reimbursable', sage_desktop_api.models.fields.BooleanFalseField(default=True, help_text='Expense reimbursable or not')),
('reimbursable', sage_desktop_api.models.fields.BooleanFalseField(default=False, help_text='Expense reimbursable or not')),
('state', sage_desktop_api.models.fields.StringNotNullField(help_text='Expense state', max_length=255)),
('vendor', sage_desktop_api.models.fields.StringNotNullField(help_text='Vendor', max_length=255)),
('vendor', sage_desktop_api.models.fields.StringNullField(help_text='Vendor', max_length=255, null=True)),
('cost_center', sage_desktop_api.models.fields.StringNullField(help_text='Fyle Expense Cost Center', max_length=255, null=True)),
('corporate_card_id', sage_desktop_api.models.fields.StringNullField(help_text='Corporate Card ID', max_length=255, null=True)),
('purpose', models.TextField(blank=True, help_text='Purpose', null=True)),
('report_id', sage_desktop_api.models.fields.StringNotNullField(help_text='Report ID', max_length=255)),
('billable', sage_desktop_api.models.fields.BooleanFalseField(default=True, help_text='Expense billable or not')),
('billable', sage_desktop_api.models.fields.BooleanFalseField(default=False, help_text='Expense billable or not')),
('file_ids', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), help_text='File IDs', null=True, size=None)),
('spent_at', sage_desktop_api.models.fields.CustomDateTimeField(help_text='Expense spent at', null=True)),
('approved_at', sage_desktop_api.models.fields.CustomDateTimeField(help_text='Expense approved at', null=True)),
Expand All @@ -72,15 +72,37 @@ class Migration(migrations.Migration):
('fund_source', sage_desktop_api.models.fields.StringNotNullField(help_text='Expense fund source', max_length=255)),
('verified_at', sage_desktop_api.models.fields.CustomDateTimeField(help_text='Report verified at', null=True)),
('custom_properties', sage_desktop_api.models.fields.CustomJsonField(default=list, help_text='Custom Properties', null=True)),
('report_title', models.TextField(blank=True, help_text='Report title', null=True)),
('payment_number', sage_desktop_api.models.fields.StringNullField(help_text='Expense payment number', max_length=55, null=True)),
('tax_amount', sage_desktop_api.models.fields.FloatNullField(help_text='Tax Amount', null=True)),
('tax_group_id', sage_desktop_api.models.fields.StringNullField(help_text='Tax Group ID', max_length=255, null=True)),
('exported', sage_desktop_api.models.fields.BooleanFalseField(default=True, help_text='Expense reimbursable or not')),
('previous_export_state', sage_desktop_api.models.fields.StringNullField(help_text='Previous export state', max_length=255, null=True)),
('accounting_export_summary', sage_desktop_api.models.fields.CustomJsonField(default=list, help_text='Accounting Export Summary', null=True)),
('workspace', models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')),
('workspace', models.ForeignKey(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')),
],
options={
'db_table': 'expenses',
},
),
migrations.CreateModel(
name='DependentFieldSetting',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at datetime')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Updated at datetime')),
('id', models.AutoField(primary_key=True, serialize=False)),
('is_import_enabled', sage_desktop_api.models.fields.BooleanFalseField(default=False, help_text='Is Import Enabled')),
('project_field_id', sage_desktop_api.models.fields.IntegerNotNullField(help_text='Fyle Source Field ID')),
('cost_code_field_name', sage_desktop_api.models.fields.StringNotNullField(help_text='Fyle Cost Code Field Name', max_length=255)),
('cost_code_field_id', sage_desktop_api.models.fields.StringNotNullField(help_text='Fyle Cost Code Field ID', max_length=255)),
('cost_code_placeholder', models.TextField(blank=True, help_text='Placeholder for Cost code', null=True)),
('cost_category_field_name', sage_desktop_api.models.fields.StringNotNullField(help_text='Fyle Cost Category Field Name', max_length=255)),
('cost_category_field_id', sage_desktop_api.models.fields.StringNotNullField(help_text='Fyle Cost Category Field ID', max_length=255)),
('cost_category_placeholder', models.TextField(blank=True, help_text='Placeholder for Cost Category', null=True)),
('last_successful_import_at', sage_desktop_api.models.fields.CustomDateTimeField(help_text='Last Successful Import At', null=True)),
('workspace', models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')),
],
options={
'db_table': 'dependent_field_settings',
},
),
]
37 changes: 0 additions & 37 deletions apps/fyle/migrations/0002_dependentfieldsetting.py

This file was deleted.

Loading

0 comments on commit 5b1935d

Please sign in to comment.