From d90c28af2df6c4c85c30f9b9dab8537cb07e86a8 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 6 Nov 2023 14:44:04 +0530 Subject: [PATCH] add support for accounting export and expense cretions --- apps/accounting_exports/models.py | 89 ++++++++++++++++++++++++++++++- apps/fyle/models.py | 64 ++++++++++++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/apps/accounting_exports/models.py b/apps/accounting_exports/models.py index 3c703bee..e0912825 100644 --- a/apps/accounting_exports/models.py +++ b/apps/accounting_exports/models.py @@ -1,5 +1,11 @@ +from datetime import datetime +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.contrib.postgres.fields.jsonb import KeyTextTransform + +from django.db.models import Count from fyle_accounting_mappings.models import ExpenseAttribute @@ -13,9 +19,16 @@ StringOptionsField, IntegerNullField ) -from apps.workspaces.models import BaseForeignWorkspaceModel, BaseModel +from apps.workspaces.models import BaseForeignWorkspaceModel, BaseModel, ExportSetting from apps.fyle.models import Expense + +ALLOWED_FIELDS = [ + 'employee_email', 'report_id', 'claim_number', 'settlement_id', + 'fund_source', 'vendor', 'category', 'project', 'cost_center', + 'verified_at', 'approved_at', 'spent_at', 'expense_id', 'expense_number', 'payment_number', 'posted_at' +] + TYPE_CHOICES = ( ('INVOICES', 'INVOICES'), ('DIRECT_COST', 'DIRECT_COST'), @@ -30,6 +43,30 @@ ('AUTO', 'AUTO') ) +ALLOWED_FORM_INPUT = { + 'group_expenses_by': ['settlement_id', 'claim_number', 'report_id', 'category', 'vendor', 'expense_id', 'expense_number', 'payment_number'], + 'export_date_type': ['current_date', 'approved_at', 'spent_at', 'verified_at', 'last_spent_at', 'posted_at'] +} + + +def _group_expenses(expenses, group_fields, workspace_id): + expense_ids = list(map(lambda expense: expense.id, expenses)) + expenses = Expense.objects.filter(id__in=expense_ids).all() + + custom_fields = {} + + for field in group_fields: + if field.lower() not in ALLOWED_FIELDS: + group_fields.pop(group_fields.index(field)) + field = ExpenseAttribute.objects.filter(workspace_id=workspace_id, + attribute_type=field.upper()).first() + if field: + custom_fields[field.attribute_type.lower()] = KeyTextTransform(field.display_name, 'custom_properties') + + expense_groups = list(expenses.values(*group_fields, **custom_fields).annotate( + total=Count('*'), expense_ids=ArrayAgg('id'))) + return expense_groups + class AccountingExport(BaseForeignWorkspaceModel): """ @@ -50,6 +87,56 @@ class AccountingExport(BaseForeignWorkspaceModel): class Meta: db_table = 'accounting_exports' + @staticmethod + def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense], workspace_id): + """ + Group expense by and fund_source + """ + export_setting = ExportSetting.objects.get(workspace_id=workspace_id) + + reimbursable_expense_group_fields = export_setting.reimbursable_expense_grouped_by + reimbursable_expenses = list(filter(lambda expense: expense.fund_source == 'PERSONAL', expense_objects)) + + expense_groups = _group_expenses(reimbursable_expenses, reimbursable_expense_group_fields, workspace_id) + + corporate_credit_card_expense_group_field = export_setting.credit_card_expense_grouped_by + corporate_credit_card_expenses = list(filter(lambda expense: expense.fund_source == 'CCC', expense_objects)) + corporate_credit_card_expense_groups = _group_expenses( + corporate_credit_card_expenses, corporate_credit_card_expense_group_field, workspace_id) + + expense_groups.extend(corporate_credit_card_expense_groups) + + for expense_group in expense_groups: + if export_setting.reimbursable_expense_date == 'last_spent_at': + expense_group['last_spent_at'] = Expense.objects.filter(id__in=expense_group['expense_ids']).order_by('-spent_at').first().spent_at + + if export_setting.credit_card_expense_date == 'last_spent_at': + expense_group['last_spent_at'] = Expense.objects.filter(id__in=expense_group['expense_ids']).order_by('-spent_at').first().spent_at + + employee_name = Expense.objects.filter( + id__in=expense_group['expense_ids'] + ).first().employee_name + + expense_ids = expense_group['expense_ids'] + expense_group.pop('total') + expense_group.pop('expense_ids') + + for key in expense_group: + if key in ALLOWED_FORM_INPUT['export_date_type']: + if expense_group[key]: + expense_group[key] = expense_group[key].strftime('%Y-%m-%dT%H:%M:%S') + else: + expense_group[key] = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + + expense_group_object = AccountingExport.objects.create( + workspace_id=workspace_id, + fund_source=expense_group['fund_source'], + description=expense_group, + employee_name=employee_name + ) + + expense_group_object.expenses.add(*expense_ids) + class Error(BaseForeignWorkspaceModel): """ diff --git a/apps/fyle/models.py b/apps/fyle/models.py index ee7a18e0..6aba9db2 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -1,5 +1,7 @@ +from typing import List, Dict from django.db import models from django.contrib.postgres.fields import ArrayField + from sage_desktop_api.models.fields import ( StringNotNullField, StringNullField, @@ -13,6 +15,7 @@ IntegerNotNullField, ) from apps.workspaces.models import BaseModel, BaseForeignWorkspaceModel +from apps.accounting_exports.models import AccountingExport EXPENSE_FILTER_RANK = ( @@ -41,6 +44,11 @@ ('not_in', 'not_in') ) +SOURCE_ACCOUNT_MAP = { + 'PERSONAL_CASH_ACCOUNT': 'PERSONAL', + 'PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT': 'CCC' +} + class ExpenseFilter(BaseForeignWorkspaceModel): """ @@ -104,6 +112,62 @@ class Expense(BaseModel): class Meta: db_table = 'expenses' + @staticmethod + def create_expense_objects(expenses: List[Dict], workspace_id: int): + """ + Bulk create expense objects + """ + expense_objects = [] + + for expense in expenses: + for custom_property_field in expense['custom_properties']: + if expense['custom_properties'][custom_property_field] == '': + expense['custom_properties'][custom_property_field] = None + expense_object, _ = Expense.objects.update_or_create( + expense_id=expense['id'], + defaults={ + 'employee_email': expense['employee_email'], + 'employee_name': expense['employee_name'], + 'category': expense['category'], + 'sub_category': expense['sub_category'], + 'project': expense['project'], + 'expense_number': expense['expense_number'], + 'org_id': expense['org_id'], + 'claim_number': expense['claim_number'], + 'amount': round(expense['amount'], 2), + 'currency': expense['currency'], + 'foreign_amount': expense['foreign_amount'], + 'foreign_currency': expense['foreign_currency'], + 'tax_amount': expense['tax_amount'], + 'tax_group_id': expense['tax_group_id'], + 'settlement_id': expense['settlement_id'], + 'reimbursable': expense['reimbursable'], + 'billable': expense['billable'], + 'state': expense['state'], + 'vendor': expense['vendor'][:250] if expense['vendor'] else None, + 'cost_center': expense['cost_center'], + 'purpose': expense['purpose'], + 'report_id': expense['report_id'], + 'report_title': expense['report_title'], + 'spent_at': expense['spent_at'], + 'approved_at': expense['approved_at'], + 'posted_at': expense['posted_at'], + 'expense_created_at': expense['expense_created_at'], + 'expense_updated_at': expense['expense_updated_at'], + 'fund_source': SOURCE_ACCOUNT_MAP[expense['source_account_type']], + 'verified_at': expense['verified_at'], + 'custom_properties': expense['custom_properties'], + 'payment_number': expense['payment_number'], + 'file_ids': expense['file_ids'], + 'corporate_card_id': expense['corporate_card_id'], + } + ) + + if not AccountingExport.objects.filter(expenses__id=expense_object.id).first(): + expense_objects.append(expense_object) + + return expense_objects + class DependentFieldSetting(BaseModel): """