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

QBD CCC Mapping commit #58

Merged
merged 17 commits into from
Sep 22, 2023
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ disable=print-statement,
too-many-locals,
too-few-public-methods,
super-with-arguments,
too-many-instance-attributes
too-many-instance-attributes,
consider-using-f-string

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
2 changes: 1 addition & 1 deletion apps/fyle/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def import_credit_card_expenses(workspace_id, accounting_export: AccountingExpor
state=export_settings.credit_card_expense_state,
settled_at=last_synced_at if export_settings.credit_card_expense_state == 'PAYMENT_PROCESSING' else None,
approved_at=last_synced_at if export_settings.credit_card_expense_state == 'APPROVED' else None,
filter_credit_expenses=True,
filter_credit_expenses=False,
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
last_paid_at=last_synced_at if export_settings.credit_card_expense_state == 'PAID' else None
)

Expand Down
Empty file added apps/mappings/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions apps/mappings/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions apps/mappings/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class MappingsConfig(AppConfig):
name = 'apps.mappings'
49 changes: 49 additions & 0 deletions apps/mappings/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import logging
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved

from django.conf import settings
from fyle.platform import Platform

from apps.workspaces.models import FyleCredential

from .models import QBDMapping

logger = logging.getLogger(__name__)

def sync_card(workspace_id: int):
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
try:
fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id)
platform = Platform(
NileshPant1999 marked this conversation as resolved.
Show resolved Hide resolved
server_url='{}/platform/v1beta'.format(fyle_credentials.cluster_domain),
token_url=settings.FYLE_TOKEN_URI,
client_id=settings.FYLE_CLIENT_ID,
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
client_secret=settings.FYLE_CLIENT_SECRET,
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
refresh_token=fyle_credentials.refresh_token,
)
query = {
'order': 'updated_at.desc',
Copy link
Contributor

Choose a reason for hiding this comment

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

wrong indentation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

indentations are crt @Shwetabhk , some time in git it shows like this, but the indentations are correct, if it is wrong how it would execute, it will thrown errors

}
Copy link
Contributor

Choose a reason for hiding this comment

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

wrong indentation

generator = platform.v1beta.admin.corporate_cards.list_all(query)
for items in generator:
card_attributes = []
unique_card_numbers = []
for card in items['data']:
value = '{} - {}'.format(
card['bank_name'],
card['card_number'][-6:].replace('-', '')
)

if value not in unique_card_numbers:
unique_card_numbers.append(value)
card_attributes.append({
'attribute_type': 'CORPORATE_CARD',
'value': value,
'source_id': card['id'],
})
if len(card_attributes) > 0:
QBDMapping.update_or_create_mapping_objects(card_attributes, workspace_id)

except FyleCredential.DoesNotExist:
logger.info('Fyle credentials not found %s', workspace_id)

except Exception:
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
logger.exception('Something unexpected happened workspace_id: %s', workspace_id)
34 changes: 34 additions & 0 deletions apps/mappings/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.1.14 on 2023-08-25 11:14

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('workspaces', '0025_auto_20230825_1040'),
]

operations = [
migrations.CreateModel(
name='QBDMapping',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('attribute_type', models.CharField(help_text='Type of expense attribute', max_length=255)),
('source_value', models.CharField(help_text='Value of expense attribute', max_length=1000)),
('source_id', models.CharField(help_text='Fyle ID', max_length=255)),
('destination_value', models.CharField(blank=True,
help_text='Value of destination attribute', max_length=1000, null=True)),
('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')),
('workspace', models.ForeignKey(help_text='Reference to Workspace model',
on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')),
],
options={
'db_table': 'qbd_mappings',
},
),
]
Empty file.
33 changes: 33 additions & 0 deletions apps/mappings/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import List, Dict
from django.db import models

from apps.workspaces.models import Workspace

class QBDMapping(models.Model):
"""
Fyle Expense Attributes
"""
id = models.AutoField(primary_key=True)
attribute_type = models.CharField(max_length=255, help_text='Type of expense attribute')
source_value = models.CharField(max_length=1000, help_text='Value of expense attribute')
source_id = models.CharField(max_length=255, help_text='Fyle ID')
destination_value = models.CharField(max_length=1000,
null=True, blank=True, help_text='Value of destination attribute')
workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model')
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')

class Meta:
db_table = 'qbd_mappings'

@staticmethod
def update_or_create_mapping_objects(qbd_mapping_objects: List[Dict],workspace_id: int):
for qbd_mapping_object in qbd_mapping_objects:
QBDMapping.objects.update_or_create(
workspace_id= workspace_id,
source_value= qbd_mapping_object['value'],
attribute_type= qbd_mapping_object['attribute_type'],
defaults={
'source_id': qbd_mapping_object['source_id'],
}
)
22 changes: 22 additions & 0 deletions apps/mappings/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from rest_framework import serializers

from .models import QBDMapping


class QBDMappingSerializer(serializers.ModelSerializer):
class Meta:
model = QBDMapping
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
fields = '__all__'
read_only_fields = ('workspace', 'created_at', 'updated_at')

def create(self, validated_data):
workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id')
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
qbd_mapping, _ = QBDMapping.objects.update_or_create(
source_id=validated_data['source_id'],
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
workspace_id=workspace_id,
defaults={
'destination_value': validated_data['destination_value']
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
}
)

return qbd_mapping
5 changes: 5 additions & 0 deletions apps/mappings/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .helpers import sync_card

DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
def sync_attributes(attribute_type: str, workspace_id: int):
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
if attribute_type == 'CORPORATE_CARD':
sync_card(workspace_id)
26 changes: 26 additions & 0 deletions apps/mappings/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""quickbooks_desktop_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 .views import (
QBDMappingView,
QBDMappingStatsView
)


urlpatterns = [
path('', QBDMappingView.as_view(), name='QBD-Mapping'),
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
path('stats/', QBDMappingStatsView.as_view(), name='QBD-Mapping-stats'),
]
Empty file added apps/mappings/utils.py
Empty file.
56 changes: 56 additions & 0 deletions apps/mappings/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.views import status

from apps.mappings.serializers import QBDMappingSerializer
from quickbooks_desktop_api.utils import assert_valid, LookupFieldMixin

from .models import QBDMapping
from .tasks import sync_attributes

DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
class QBDMappingView(LookupFieldMixin, generics.ListCreateAPIView):
"""
QBD Mapping Update View
"""
queryset = QBDMapping.objects.all()
serializer_class = QBDMappingSerializer
lookup_field = 'workspace_id'
lookup_url_kwarg = 'workspace_id'
filter_backends = (DjangoFilterBackend,)
filterset_fields = {'attribute_type': {'exact'},'destination_value': {'isnull'}}

def get(self, request, *args, **kwargs):
attribute_type = self.request.query_params.get('attribute_type')
workspace_id= self.kwargs['workspace_id']
sync_attributes(attribute_type, workspace_id)

return self.list(request, *args, **kwargs)

DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
#mapping stats view
class QBDMappingStatsView(generics.RetrieveAPIView):
"""
Stats for total mapped and unmapped count for a given attribute type
"""
def get(self, request, *args, **kwargs):
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
source_type = self.request.query_params.get('source_type')
workspace_id= self.kwargs['workspace_id']

assert_valid(source_type is not None, 'query param source_type not found')

total_attributes_count = QBDMapping.objects.filter(
workspace_id=workspace_id,
attribute_type = source_type).count()

unmapped_attributes_count = QBDMapping.objects.filter(
workspace_id=workspace_id,
attribute_type = source_type,
destination_value__isnull=True).count()

return Response(
data={
'all_attributes_count': total_attributes_count,
'unmapped_attributes_count': unmapped_attributes_count
},
status=status.HTTP_200_OK
)
21 changes: 21 additions & 0 deletions apps/workspaces/migrations/0025_auto_20230825_1040.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.1.14 on 2023-08-25 10:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('workspaces', '0024_exportsettings_is_simplify_report_closure_enabled'),
]

operations = [
migrations.AlterField(
model_name='exportsettings',
name='credit_card_expense_date',
field=models.CharField(choices=[('last_spent_at', 'last_spent_at'),
('spent_at', 'spent_at'),
('posted_at', 'posted_at'),
('created_at', 'created_at')], max_length=255, null=True),
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved
),
]
14 changes: 14 additions & 0 deletions apps/workspaces/migrations/0026_merge_20230901_0829.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 3.1.14 on 2023-09-01 08:29

from django.db import migrations


class Migration(migrations.Migration):
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved

dependencies = [
('workspaces', '0025_auto_20230823_1118'),
('workspaces', '0025_auto_20230825_1040'),
]

operations = [
]
3 changes: 2 additions & 1 deletion apps/workspaces/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
path('<int:workspace_id>/advanced_settings/', AdvancedSettingView.as_view(), name='advanced-settings'),
path('<int:workspace_id>/field_mappings/', FieldMappingView.as_view(), name='field-mappings'),
path('<int:workspace_id>/trigger_export/', TriggerExportView.as_view(), name='trigger-export'),
path('<int:workspace_id>/accounting_exports/', include('apps.tasks.urls'))
path('<int:workspace_id>/accounting_exports/', include('apps.tasks.urls')),
path('<int:workspace_id>/qbd_mappings/', include('apps.mappings.urls'))
]
5 changes: 4 additions & 1 deletion quickbooks_desktop_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@
'fyle_rest_auth',
'fyle_accounting_mappings',
'django_q',
'django_filters',
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved

# Created Apps
'apps.users',
'apps.workspaces',
'apps.fyle',
'apps.tasks',
'apps.qbd'
'apps.qbd',
'apps.mappings'
]

MIDDLEWARE = [
Expand Down Expand Up @@ -111,6 +113,7 @@
'fyle_rest_auth.authentication.FyleJWTAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'PAGE_SIZE': 100
}

Expand Down
2 changes: 2 additions & 0 deletions quickbooks_desktop_api/tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@
'fyle_rest_auth',
'fyle_accounting_mappings',
'django_q',
'django_filters',
DhaaraniCIT marked this conversation as resolved.
Show resolved Hide resolved

# Created Apps
'apps.users',
'apps.workspaces',
'apps.fyle',
'apps.tasks',
'apps.mappings',
'apps.qbd'
]

Expand Down
10 changes: 10 additions & 0 deletions quickbooks_desktop_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ def assert_valid(condition: bool, message: str) -> Response or None:
raise ValidationError(detail={
'message': message
})

class LookupFieldMixin:
lookup_field = 'workspace_id'

def filter_queryset(self, queryset):
if self.lookup_field in self.kwargs:
lookup_value = self.kwargs[self.lookup_field]
filter_kwargs = {self.lookup_field: lookup_value}
queryset = queryset.filter(**filter_kwargs)
return super().filter_queryset(queryset)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ django-rest-framework==0.1.0
djangorestframework==3.14.0
django-db-geventpool==4.0.1
django-request-logging==0.7.1
django-filter==21.1

# DjangoQ for running async tasks
django-q==1.3.4
Expand Down
Empty file added tests/test_mapping/__init__.py
Empty file.
Loading