Skip to content

Commit

Permalink
add auto create vendor feature (#533)
Browse files Browse the repository at this point in the history
* add auto create vendor feature

* sanitize the vendor name

* fix filter

* Fix JE auto-create vendor

* call sage-connector once, move func back to tasks
  • Loading branch information
Hrishabh17 committed Jul 31, 2024
1 parent 3efe15c commit 1ceaad5
Show file tree
Hide file tree
Showing 14 changed files with 223 additions and 59 deletions.
1 change: 0 additions & 1 deletion apps/fyle/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import logging
from typing import List, Union

from django.utils.module_loading import import_string
from django.conf import settings
from django.db.models import Q
from fyle_accounting_mappings.models import ExpenseAttribute
Expand Down
13 changes: 7 additions & 6 deletions apps/sage_intacct/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
from django.conf import settings
from django.db.models import Q,JSONField
from django.db import models

from django.utils.module_loading import import_string

from fyle_accounting_mappings.models import Mapping, MappingSetting, DestinationAttribute, CategoryMapping, \
EmployeeMapping

from apps.fyle.models import ExpenseGroup, Expense, ExpenseAttribute, Reimbursement, ExpenseGroupSettings, DependentFieldSetting
from apps.mappings.models import GeneralMapping

from apps.workspaces.models import Configuration, Workspace, FyleCredential
from apps.workspaces.models import Configuration, Workspace, FyleCredential, SageIntacctCredential
from typing import Dict, List, Union


Expand Down Expand Up @@ -873,9 +873,9 @@ class JournalEntryLineitem(models.Model):

class Meta:
db_table = 'journal_entry_lineitems'

@staticmethod
def create_journal_entry_lineitems(expense_group: ExpenseGroup, configuration: Configuration):
def create_journal_entry_lineitems(expense_group: ExpenseGroup, configuration: Configuration, sage_intacct_connection):
"""
Create journal entry lineitems
:param expense_group: expense group
Expand Down Expand Up @@ -934,8 +934,9 @@ def create_journal_entry_lineitems(expense_group: ExpenseGroup, configuration: C

vendor_id = entity.destination_vendor.destination_id if employee_mapping_setting == 'VENDOR' else None

if lineitem.fund_source == 'CCC' and configuration.use_merchant_in_journal_line and lineitem.vendor:
vendor = DestinationAttribute.objects.filter(attribute_type='VENDOR', value__iexact=lineitem.vendor, workspace_id=expense_group.workspace_id).order_by('-updated_at').first()
if lineitem.fund_source == 'CCC' and configuration.use_merchant_in_journal_line:
# here it would create a Credit Card Vendor if the expene vendor is not present
vendor = import_string('apps.sage_intacct.tasks.get_or_create_credit_card_vendor')(expense_group.workspace_id, configuration, lineitem.vendor, sage_intacct_connection)
if vendor:
vendor_id = vendor.destination_id

Expand Down
54 changes: 34 additions & 20 deletions apps/sage_intacct/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,29 @@
)

from fyle_intacct_api.exceptions import BulkError
from apps.fyle.models import ExpenseGroup, ExpenseGroupSettings, Reimbursement, Expense
from apps.fyle.models import ExpenseGroup, Expense
from apps.tasks.models import TaskLog, Error
from apps.mappings.models import GeneralMapping
from apps.fyle.actions import update_expenses_in_progress, update_failed_expenses, update_complete_expenses
from apps.fyle.tasks import post_accounting_export_summary
from apps.workspaces.models import (
SageIntacctCredential,
FyleCredential,
Configuration,
SageIntacctCredential,
FyleCredential,
Configuration,
LastExportDetail,
Workspace
)
from apps.sage_intacct.models import (
ExpenseReport,
ExpenseReportLineitem,
ExpenseReport,
ExpenseReportLineitem,
Bill,
BillLineitem,
BillLineitem,
ChargeCardTransaction,
ChargeCardTransactionLineitem,
APPayment,
APPaymentLineitem,
JournalEntry,
JournalEntryLineitem,
ChargeCardTransactionLineitem,
APPayment,
APPaymentLineitem,
JournalEntry,
JournalEntryLineitem,
SageIntacctReimbursement,
SageIntacctReimbursementLineitem
)
Expand Down Expand Up @@ -224,20 +224,34 @@ def create_or_update_employee_mapping(expense_group: ExpenseGroup, sage_intacct_
)


def get_or_create_credit_card_vendor(merchant: str, workspace_id: int):
def get_or_create_credit_card_vendor(workspace_id: int, configuration: Configuration, merchant: str = None, sage_intacct_connection: SageIntacctConnector = None):
"""
Get or create default vendor
:param merchant: Fyle Expense Merchant
:param workspace_id: Workspace Id
:return:
"""
sage_intacct_credentials = SageIntacctCredential.objects.get(workspace_id=workspace_id)
sage_intacct_connection = SageIntacctConnector(sage_intacct_credentials, workspace_id)
if not sage_intacct_connection:
sage_intacct_credentials = SageIntacctCredential.objects.get(workspace_id=workspace_id)
sage_intacct_connection = SageIntacctConnector(sage_intacct_credentials, workspace_id)

vendor = None

if merchant:
if (
merchant
and not configuration.import_vendors_as_merchants
and configuration.corporate_credit_card_expenses_object
and configuration.auto_create_merchants_as_vendors
and (
configuration.corporate_credit_card_expenses_object == 'CHARGE_CARD_TRANSACTION'
or (
configuration.corporate_credit_card_expenses_object == 'JOURNAL_ENTRY'
and configuration.use_merchant_in_journal_line
)
)
):
try:
vendor = sage_intacct_connection.get_or_create_vendor(merchant, create=False)
vendor = sage_intacct_connection.get_or_create_vendor(merchant, create=True)
except WrongParamsError as bad_request:
logger.info(bad_request.response)

Expand Down Expand Up @@ -553,7 +567,7 @@ def create_journal_entry(expense_group: ExpenseGroup, task_log_id: int, last_exp
)
else:
merchant = expense_group.expenses.first().vendor
get_or_create_credit_card_vendor(merchant, expense_group.workspace_id)
get_or_create_credit_card_vendor(expense_group.workspace_id, configuration, merchant, sage_intacct_connection)

__validate_employee_mapping(expense_group, configuration)
logger.info('Validated Employee mapping %s successfully', expense_group.id)
Expand All @@ -568,7 +582,7 @@ def create_journal_entry(expense_group: ExpenseGroup, task_log_id: int, last_exp

journal_entry_object = JournalEntry.create_journal_entry(expense_group, task_log.supdoc_id)

journal_entry_lineitem_object = JournalEntryLineitem.create_journal_entry_lineitems(expense_group, configuration)
journal_entry_lineitem_object = JournalEntryLineitem.create_journal_entry_lineitems(expense_group, configuration, sage_intacct_connection)

created_journal_entry = sage_intacct_connection.post_journal_entry(journal_entry_object, journal_entry_lineitem_object)
logger.info('Created Journal Entry with Expense Group %s successfully', expense_group.id)
Expand Down Expand Up @@ -950,7 +964,7 @@ def create_charge_card_transaction(expense_group: ExpenseGroup, task_log_id: int
sage_intacct_connection = SageIntacctConnector(sage_intacct_credentials, expense_group.workspace_id)

merchant = expense_group.expenses.first().vendor
vendor = get_or_create_credit_card_vendor(merchant, expense_group.workspace_id)
vendor = get_or_create_credit_card_vendor(expense_group.workspace_id, configuration, merchant, sage_intacct_connection)

vendor_id = vendor.destination_id if vendor else None
__validate_employee_mapping(expense_group, configuration)
Expand Down
23 changes: 20 additions & 3 deletions apps/sage_intacct/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import re
import logging
import base64
from typing import List, Dict
from datetime import datetime, timedelta
import unidecode
import time
from django.conf import settings

from cryptography.fernet import Fernet
Expand Down Expand Up @@ -648,6 +647,7 @@ def get_or_create_vendor(self, vendor_name: str, email: str = None, create: bool
:param create: False to just Get and True to Get or Create if not exists
:return: Vendor
"""
vendor_name = self.sanitize_vendor_name(vendor_name)
vendor_from_db = DestinationAttribute.objects.filter(workspace_id=self.workspace_id, attribute_type='VENDOR', value=vendor_name, active=True).first()

if vendor_from_db:
Expand All @@ -663,7 +663,7 @@ def get_or_create_vendor(self, vendor_name: str, email: str = None, create: bool
vendor = sorted_vendor_data[0]
else:
vendor = vendor['VENDOR'][0]

vendor = vendor if vendor['STATUS'] == 'active' else None
else:
vendor = None
Expand Down Expand Up @@ -1545,3 +1545,20 @@ def post_sage_intacct_reimbursement(self, reimbursement: SageIntacctReimbursemen
reimbursement_payload = self.__construct_sage_intacct_reimbursement(reimbursement, reimbursement_lineitems)
created_reimbursement = self.connection.reimbursements.post(reimbursement_payload)
return created_reimbursement

def sanitize_vendor_name(self, vendor_name: str = None) -> str:
"""
Remove special characters from Vendor Name
:param vendor_name: Vendor Name
:return: Sanitized Vendor Name
"""
sanitized_name = None
if vendor_name:
pattern = r'[!@#$%^&*()\-_=\+\[\]{}|\\:;"\'<>,.?/~`]'
sanitized_name = re.sub(pattern, '', vendor_name)
sanitized_name = re.sub(r'\s+', ' ', sanitized_name).strip()

if sanitized_name:
return sanitized_name

return None
9 changes: 4 additions & 5 deletions apps/workspaces/apis/advanced_settings/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class Meta:
'sync_fyle_to_sage_intacct_payments',
'sync_sage_intacct_to_fyle_payments',
'auto_create_destination_entity',
'memo_structure'
'memo_structure',
'auto_create_merchants_as_vendors'
]


Expand All @@ -57,7 +58,6 @@ class Meta:
'use_intacct_employee_locations'
]


def get_default_location(self, instance):
return {
'name': instance.default_location_name,
Expand Down Expand Up @@ -127,11 +127,9 @@ class Meta:
]
read_only_fields = ['workspace_id']


def get_workspace_id(self, instance):
return instance.id


def update(self, instance, validated):
configurations = validated.pop('configurations')
general_mappings = validated.pop('general_mappings')
Expand All @@ -144,7 +142,8 @@ def update(self, instance, validated):
'sync_sage_intacct_to_fyle_payments': configurations.get('sync_sage_intacct_to_fyle_payments'),
'auto_create_destination_entity': configurations.get('auto_create_destination_entity'),
'change_accounting_period': configurations.get('change_accounting_period'),
'memo_structure': configurations.get('memo_structure')
'memo_structure': configurations.get('memo_structure'),
'auto_create_merchants_as_vendors': configurations.get('auto_create_merchants_as_vendors')
}
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2024-07-26 17:19

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('workspaces', '0034_configuration_is_journal_credit_billable'),
]

operations = [
migrations.AddField(
model_name='configuration',
name='auto_create_merchants_as_vendors',
field=models.BooleanField(default=False, help_text='Auto create merchants as vendors in sage intacct'),
),
]
3 changes: 2 additions & 1 deletion apps/workspaces/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ class Configuration(models.Model):
change_accounting_period = models.BooleanField(default=False, help_text='Change the accounting period')
import_vendors_as_merchants = models.BooleanField(default=False, help_text='Auto import vendors from sage intacct '
'as merchants to Fyle')

use_merchant_in_journal_line = models.BooleanField(default=False, help_text='Export merchant as vendor in journal entry line item')
auto_create_merchants_as_vendors = models.BooleanField(default=False, help_text='Auto create merchants as vendors in sage intacct')
created_at = models.DateTimeField(auto_now_add=True, help_text='Created at')
updated_at = models.DateTimeField(auto_now=True, help_text='Updated at')

Expand Down
12 changes: 7 additions & 5 deletions tests/sql_fixtures/reset_db_fixtures/reset_db.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- PostgreSQL database dump
--

-- Dumped from database version 15.7 (Debian 15.7-1.pgdg120+1)
-- Dumped from database version 15.6 (Debian 15.6-1.pgdg120+2)
-- Dumped by pg_dump version 15.7 (Debian 15.7-1.pgdg120+1)

SET statement_timeout = 0;
Expand Down Expand Up @@ -397,7 +397,8 @@ CREATE TABLE public.configurations (
employee_field_mapping character varying(50),
is_simplify_report_closure_enabled boolean NOT NULL,
use_merchant_in_journal_line boolean NOT NULL,
is_journal_credit_billable boolean NOT NULL
is_journal_credit_billable boolean NOT NULL,
auto_create_merchants_as_vendors boolean NOT NULL
);


Expand Down Expand Up @@ -2882,8 +2883,8 @@ COPY public.charge_card_transactions (id, charge_card_id, description, supdoc_id
-- Data for Name: configurations; Type: TABLE DATA; Schema: public; Owner: postgres
--

COPY public.configurations (id, reimbursable_expenses_object, created_at, updated_at, workspace_id, corporate_credit_card_expenses_object, import_projects, sync_fyle_to_sage_intacct_payments, sync_sage_intacct_to_fyle_payments, auto_map_employees, import_categories, auto_create_destination_entity, memo_structure, import_tax_codes, change_accounting_period, import_vendors_as_merchants, employee_field_mapping, is_simplify_report_closure_enabled, use_merchant_in_journal_line, is_journal_credit_billable) FROM stdin;
1 BILL 2022-09-20 08:39:32.015647+00 2022-09-20 08:46:24.926422+00 1 BILL t t f EMAIL f t {employee_email,category,spent_on,report_number,purpose,expense_link} t t t VENDOR f f t
COPY public.configurations (id, reimbursable_expenses_object, created_at, updated_at, workspace_id, corporate_credit_card_expenses_object, import_projects, sync_fyle_to_sage_intacct_payments, sync_sage_intacct_to_fyle_payments, auto_map_employees, import_categories, auto_create_destination_entity, memo_structure, import_tax_codes, change_accounting_period, import_vendors_as_merchants, employee_field_mapping, is_simplify_report_closure_enabled, use_merchant_in_journal_line, is_journal_credit_billable, auto_create_merchants_as_vendors) FROM stdin;
1 BILL 2022-09-20 08:39:32.015647+00 2022-09-20 08:46:24.926422+00 1 BILL t t f EMAIL f t {employee_email,category,spent_on,report_number,purpose,expense_link} t t t VENDOR f f t f
\.


Expand Down Expand Up @@ -4111,6 +4112,7 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin;
183 fyle 0031_expense_paid_on_fyle 2024-06-05 16:26:11.775475+00
184 workspaces 0034_configuration_is_journal_credit_billable 2024-06-19 07:16:22.418147+00
185 fyle 0032_auto_20240703_1818 2024-07-03 18:29:14.061756+00
186 workspaces 0035_configuration_auto_create_merchants_as_vendors 2024-07-26 17:26:19.583422+00
\.


Expand Down Expand Up @@ -8118,7 +8120,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 50, true);
-- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
--

SELECT pg_catalog.setval('public.django_migrations_id_seq', 185, true);
SELECT pg_catalog.setval('public.django_migrations_id_seq', 186, true);


--
Expand Down
6 changes: 4 additions & 2 deletions tests/test_sageintacct/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ def create_expense_report(db):


@pytest.fixture
def create_journal_entry(db):
def create_journal_entry(db, mocker):
workspace_id = 1

expense_group = ExpenseGroup.objects.get(id=2)
workspace_general_settings = Configuration.objects.get(workspace_id=workspace_id)
journal_entry = JournalEntry.create_journal_entry(expense_group)
journal_entry_lineitems = JournalEntryLineitem.create_journal_entry_lineitems(expense_group,workspace_general_settings)
sage_intacct_connection = mocker.patch('apps.sage_intacct.utils.SageIntacctConnector')
sage_intacct_connection.return_value = mocker.Mock()
journal_entry_lineitems = JournalEntryLineitem.create_journal_entry_lineitems(expense_group,workspace_general_settings, sage_intacct_connection)

return journal_entry,journal_entry_lineitems

Expand Down
12 changes: 8 additions & 4 deletions tests/test_sageintacct/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_expense_report(db):
logger.info('General mapping not found')


def test_create_journal_entry(db, create_expense_group_expense, create_cost_type, create_dependent_field_setting):
def test_create_journal_entry(db, mocker, create_expense_group_expense, create_cost_type, create_dependent_field_setting):
workspace_id = 1

expense_group = ExpenseGroup.objects.get(id=2)
Expand All @@ -94,7 +94,9 @@ def test_create_journal_entry(db, create_expense_group_expense, create_cost_type
general_mappings.save()

journal_entry = JournalEntry.create_journal_entry(expense_group)
journal_entry_lineitems = JournalEntryLineitem.create_journal_entry_lineitems(expense_group, workspace_general_settings)
sage_intacct_connection = mocker.patch('apps.sage_intacct.utils.SageIntacctConnector')
sage_intacct_connection.return_value = mocker.Mock()
journal_entry_lineitems = JournalEntryLineitem.create_journal_entry_lineitems(expense_group, workspace_general_settings, sage_intacct_connection)

for journal_entry_lineitem in journal_entry_lineitems:
assert journal_entry_lineitem.amount == 11.0
Expand All @@ -105,7 +107,7 @@ def test_create_journal_entry(db, create_expense_group_expense, create_cost_type

try:
general_mappings.delete()
journal_entry_lineitems = JournalEntryLineitem.create_journal_entry_lineitems(expense_group, workspace_general_settings)
journal_entry_lineitems = JournalEntryLineitem.create_journal_entry_lineitems(expense_group, workspace_general_settings, sage_intacct_connection)
except:
logger.info('General mapping not found')

Expand All @@ -132,6 +134,8 @@ def test_create_charge_card_transaction(mocker, db, create_expense_group_expense
)
workspace_id = 1

configuration = Configuration.objects.get(workspace_id=workspace_id)

expense_group = ExpenseGroup.objects.get(id=1)
expense_group.description.update({'employee_email': '[email protected]'})
expense_group.save()
Expand All @@ -143,7 +147,7 @@ def test_create_charge_card_transaction(mocker, db, create_expense_group_expense
general_mappings.save()

merchant = expense_group.expenses.first().vendor
vendor = get_or_create_credit_card_vendor(merchant, expense_group.workspace_id)
vendor = get_or_create_credit_card_vendor(expense_group.workspace_id, configuration, merchant)

charge_card_transaction = ChargeCardTransaction.create_charge_card_transaction(expense_group, vendor.destination_id)
workspace_general_settings = Configuration.objects.get(workspace_id=workspace_id)
Expand Down
Loading

0 comments on commit 1ceaad5

Please sign in to comment.