Skip to content

Commit

Permalink
Workspace admin apis (#14)
Browse files Browse the repository at this point in the history
* Workspace admin apis

* Fyle accounting mappings enabled

* Accounting exports api (#15)

* Accounting exports api

* Accounting Exports count API (#16)

* Accounting Exports count API

* Accounting exports summary APIs (#17)

* Accounting exports summary APIs

* Errors APIs (#18)

* Errors APIs

* Expense filter api (#19)

* Expense Filter APIs

* Expense Filter APIs

* Expense Filter APIs

* Resolved
  • Loading branch information
ruuushhh authored Nov 7, 2023
1 parent 3882021 commit e0b6625
Show file tree
Hide file tree
Showing 22 changed files with 813 additions and 10 deletions.
89 changes: 89 additions & 0 deletions apps/accounting_exports/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField

from fyle_accounting_mappings.models import ExpenseAttribute

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

TYPE_CHOICES = (
('INVOICES', 'INVOICES'),
('DIRECT_COST', 'DIRECT_COST'),
('FETCHING_REIMBURSABLE_EXPENSES', 'FETCHING_REIMBURSABLE_EXPENSES'),
('FETCHING_CREDIT_CARD_EXPENENSES', 'FETCHING_CREDIT_CARD_EXPENENSES')
)

ERROR_TYPE_CHOICES = (('EMPLOYEE_MAPPING', 'EMPLOYEE_MAPPING'), ('CATEGORY_MAPPING', 'CATEGORY_MAPPING'), ('BUSINESS_CENTRAL_ERROR', 'BUSINESS_CENTRAL_ERROR'))

EXPORT_MODE_CHOICES = (
('MANUAL', 'MANUAL'),
('AUTO', 'AUTO')
)


class AccountingExport(BaseForeignWorkspaceModel):
"""
Table to store accounting exports
"""
id = models.AutoField(primary_key=True)
type = StringOptionsField(choices=TYPE_CHOICES, help_text='Task type')
fund_source = StringNotNullField(help_text='Expense fund source')
mapping_errors = ArrayField(help_text='Mapping errors', base_field=models.CharField(max_length=255), blank=True, null=True)
expenses = models.ManyToManyField(Expense, help_text="Expenses under this Expense Group")
task_id = StringNullField(help_text='Fyle Jobs task reference')
description = CustomJsonField(help_text='Description')
status = StringNotNullField(help_text='Task Status')
detail = CustomJsonField(help_text='Task Response')
business_central_errors = CustomJsonField(help_text='Business Central Errors')
exported_at = CustomDateTimeField(help_text='time of export')

class Meta:
db_table = 'accounting_exports'


class AccountingExportSummary(BaseModel):
"""
Table to store accounting export summary
"""
id = models.AutoField(primary_key=True)
last_exported_at = CustomDateTimeField(help_text='Last exported at datetime')
next_export_at = CustomDateTimeField(help_text='next export datetime')
export_mode = StringOptionsField(choices=EXPORT_MODE_CHOICES, help_text='Export mode')
total_accounting_export_count = IntegerNullField(help_text='Total count of accounting export exported')
successful_accounting_export_count = IntegerNullField(help_text='count of successful accounting export')
failed_accounting_export_count = IntegerNullField(help_text='count of failed accounting export')

class Meta:
db_table = 'accounting_export_summary'


class Error(BaseForeignWorkspaceModel):
"""
Table to store errors
"""
id = models.AutoField(primary_key=True)
type = StringOptionsField(max_length=50, choices=ERROR_TYPE_CHOICES, help_text='Error type')
accounting_export = models.ForeignKey(
AccountingExport, on_delete=models.PROTECT,
null=True, help_text='Reference to Expense group'
)
expense_attribute = models.OneToOneField(
ExpenseAttribute, on_delete=models.PROTECT,
null=True, help_text='Reference to Expense Attribute'
)
is_resolved = BooleanFalseField(help_text='Is resolved')
error_title = StringNotNullField(help_text='Error title')
error_detail = TextNotNullField(help_text='Error detail')

class Meta:
db_table = 'errors'
33 changes: 33 additions & 0 deletions apps/accounting_exports/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rest_framework import serializers

from apps.accounting_exports.models import AccountingExport, AccountingExportSummary, Error


class AccountingExportSerializer(serializers.ModelSerializer):
"""
Accounting Export serializer
"""

class Meta:
model = AccountingExport
fields = '__all__'


class AccountingExportSummarySerializer(serializers.ModelSerializer):
"""
Accounting Export Summary serializer
"""

class Meta:
model = AccountingExportSummary
fields = '__all__'


class ErrorSerializer(serializers.ModelSerializer):
"""
Serializer for the Errors
"""

class Meta:
model = Error
fields = '__all__'
25 changes: 25 additions & 0 deletions apps/accounting_exports/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""ms_business_central_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/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import path

from apps.accounting_exports.views import AccountingExportView, AccountingExportCountView, AccountingExportSummaryView, ErrorsView


urlpatterns = [
path('', AccountingExportView.as_view(), name='accounting-exports'),
path('count/', AccountingExportCountView.as_view(), name='accounting-exports-count'),
path('summary/', AccountingExportSummaryView.as_view(), name='accounting-exports-summary'),
path('errors/', ErrorsView.as_view(), name='errors'),
]
55 changes: 55 additions & 0 deletions apps/accounting_exports/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import logging

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics
from rest_framework.response import Response

from ms_business_central_api.utils import LookupFieldMixin
from apps.accounting_exports.serializers import AccountingExportSerializer, AccountingExportSummarySerializer, ErrorSerializer
from apps.accounting_exports.models import AccountingExport, AccountingExportSummary, Error


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


class AccountingExportView(LookupFieldMixin, generics.ListAPIView):
"""
Retrieve or Create Accounting Export
"""
serializer_class = AccountingExportSerializer
queryset = AccountingExport.objects.all().order_by("-updated_at")
filter_backends = (DjangoFilterBackend,)
filterset_fields = {"type": {"in"}, "updated_at": {"lte", "gte"}, "id": {"in"}, "status": {"in"}}


class AccountingExportCountView(generics.RetrieveAPIView):
"""
Retrieve Accounting Export Count
"""

def get(self, request, *args, **kwargs):
params = {"workspace_id": self.kwargs['workspace_id']}

if request.query_params.get("status__in"):
params["status__in"] = request.query_params.get("status__in").split(",")

return Response({"count": AccountingExport.objects.filter(**params).count()})


class AccountingExportSummaryView(generics.RetrieveAPIView):
"""
Retrieve Accounting Export Summary
"""
lookup_field = 'workspace_id'
lookup_url_kwarg = 'workspace_id'

queryset = AccountingExportSummary.objects.filter(last_exported_at__isnull=False, total_accounting_export_count__gt=0)
serializer_class = AccountingExportSummarySerializer


class ErrorsView(LookupFieldMixin, generics.ListAPIView):
serializer_class = ErrorSerializer
queryset = Error.objects.all()
filter_backends = (DjangoFilterBackend,)
filterset_fields = {"type": {"exact"}, "is_resolved": {"exact"}}
112 changes: 112 additions & 0 deletions apps/fyle/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField
from ms_business_central_api.models.fields import (
StringNotNullField,
StringNullField,
BooleanFalseField,
CustomJsonField,
CustomDateTimeField,
CustomEmailField,
FloatNullField,
StringOptionsField,
IntegerOptionsField,
)
from apps.workspaces.models import BaseModel, BaseForeignWorkspaceModel


EXPENSE_FILTER_RANK = (
(1, 1),
(2, 2)
)

EXPENSE_FILTER_JOIN_BY = (
('AND', 'AND'),
('OR', 'OR')
)

EXPENSE_FILTER_CUSTOM_FIELD_TYPE = (
('SELECT', 'SELECT'),
('NUMBER', 'NUMBER'),
('TEXT', 'TEXT')
)

EXPENSE_FILTER_OPERATOR = (
('isnull', 'isnull'),
('in', 'in'),
('iexact', 'iexact'),
('icontains', 'icontains'),
('lt', 'lt'),
('lte', 'lte'),
('not_in', 'not_in')
)


class Expense(BaseModel):
"""
Expense
"""
id = models.AutoField(primary_key=True)
employee_email = CustomEmailField(help_text='Email id of the Fyle employee')
employee_name = StringNullField(help_text='Name of the Fyle employee')
category = StringNullField(help_text='Fyle Expense Category')
sub_category = StringNullField(help_text='Fyle Expense Sub-Category')
project = StringNullField(help_text='Project')
expense_id = StringNotNullField(unique=True, help_text='Expense ID')
org_id = StringNullField(help_text='Organization ID')
expense_number = StringNotNullField(help_text='Expense Number')
claim_number = StringNotNullField(help_text='Claim Number')
amount = models.FloatField(help_text='Home Amount')
currency = StringNotNullField(max_length=5, help_text='Home Currency')
foreign_amount = models.FloatField(null=True, help_text='Foreign Amount')
foreign_currency = StringNotNullField(max_length=5, help_text='Foreign Currency')
settlement_id = StringNullField(help_text='Settlement ID')
reimbursable = BooleanFalseField(help_text='Expense reimbursable or not')
state = StringNotNullField(help_text='Expense state')
vendor = StringNotNullField(help_text='Vendor')
cost_center = StringNullField(help_text='Fyle Expense Cost Center')
corporate_card_id = StringNullField(help_text='Corporate Card ID')
purpose = models.TextField(null=True, blank=True, help_text='Purpose')
report_id = StringNotNullField(help_text='Report ID')
billable = BooleanFalseField(help_text='Expense billable or not')
file_ids = ArrayField(base_field=models.CharField(max_length=255), null=True, help_text='File IDs')
spent_at = CustomDateTimeField(help_text='Expense spent at')
approved_at = CustomDateTimeField(help_text='Expense approved at')
posted_at = CustomDateTimeField(help_text='Date when the money is taken from the bank')
expense_created_at = CustomDateTimeField(help_text='Expense created at')
expense_updated_at = CustomDateTimeField(help_text='Expense created at')
fund_source = StringNotNullField(help_text='Expense fund source')
verified_at = CustomDateTimeField(help_text='Report verified at')
custom_properties = CustomJsonField(help_text="Custom Properties")
tax_amount = FloatNullField(help_text='Tax Amount')
tax_group_id = StringNullField(help_text='Tax Group ID')
exported = BooleanFalseField(help_text='Expense reimbursable or not')
previous_export_state = StringNullField(max_length=255, help_text='Previous export state')
accounting_export_summary = CustomJsonField(default=dict, help_text='Accounting Export Summary')

class Meta:
db_table = 'expenses'


class ExpenseFilter(BaseForeignWorkspaceModel):
"""
Reimbursements
"""
id = models.AutoField(primary_key=True)
condition = StringNotNullField(help_text='Condition for the filter')
operator = StringOptionsField(choices=EXPENSE_FILTER_OPERATOR, help_text='Operator for the filter')
values = ArrayField(base_field=models.CharField(max_length=255), null=True, help_text='Values for the operator')
rank = IntegerOptionsField(choices=EXPENSE_FILTER_RANK, help_text='Rank for the filter')
join_by = StringOptionsField(choices=EXPENSE_FILTER_JOIN_BY, max_length=3, help_text='Used to join the filter (AND/OR)')
is_custom = BooleanFalseField(help_text='Custom Field or not')
custom_field_type = StringOptionsField(help_text='Custom field type', choices=EXPENSE_FILTER_CUSTOM_FIELD_TYPE)

class Meta:
db_table = 'expense_filters'


class Reimbursement:
"""
Creating a dummy class to be able to user
fyle_integrations_platform_connector correctly
"""
pass
21 changes: 21 additions & 0 deletions apps/fyle/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Fyle Serializers
"""
import logging
from rest_framework import serializers

from apps.fyle.models import ExpenseFilter

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


class ExpenseFilterSerializer(serializers.ModelSerializer):
"""
Expense Filter Serializer
"""

class Meta:
model = ExpenseFilter
fields = '__all__'
read_only_fields = ('id', 'workspace', 'created_at', 'updated_at')
Empty file removed apps/fyle/serializers.py.py
Empty file.
24 changes: 24 additions & 0 deletions apps/fyle/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""fyle_ms_business_central URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""

from django.urls import path
from apps.fyle.views import ExpenseFilterView, ExpenseFilterDeleteView


urlpatterns = [
path('expense_filters/<int:pk>/', ExpenseFilterDeleteView.as_view(), name='expense-filters'),
path('expense_filters/', ExpenseFilterView.as_view(), name='expense-filters'),
]
26 changes: 26 additions & 0 deletions apps/fyle/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging
from rest_framework import generics
from ms_business_central_api.utils import LookupFieldMixin
from apps.fyle.serializers import ExpenseFilterSerializer
from apps.fyle.models import ExpenseFilter

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


class ExpenseFilterView(LookupFieldMixin, generics.ListCreateAPIView):
"""
Expense Filter view
"""

queryset = ExpenseFilter.objects.all()
serializer_class = ExpenseFilterSerializer


class ExpenseFilterDeleteView(generics.DestroyAPIView):
"""
Expense Filter Delete view
"""

queryset = ExpenseFilter.objects.all()
serializer_class = ExpenseFilterSerializer
Loading

0 comments on commit e0b6625

Please sign in to comment.