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

feat: implement split expense grouping functionality #675

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions apps/fyle/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
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', 'posted_at'
'verified_at', 'approved_at', 'spent_at', 'expense_id', 'posted_at',
'bank_transaction_id'
]


Expand Down Expand Up @@ -375,6 +376,7 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense
"""
expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id)

# Group Reimbursable Expenses
reimbursable_expense_group_fields = expense_group_settings.reimbursable_expense_group_fields

reimbursable_expenses = list(filter(lambda expense: expense.fund_source == 'PERSONAL', expense_objects))
Expand Down Expand Up @@ -405,6 +407,8 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense
reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses))

expense_groups = _group_expenses(reimbursable_expenses, reimbursable_expense_group_fields, workspace_id)

# Group CCC Expenses
corporate_credit_card_expense_group_field = expense_group_settings.corporate_credit_card_expense_group_fields

corporate_credit_card_expenses = list(filter(lambda expense: expense.fund_source == 'CCC', expense_objects))
Expand All @@ -414,10 +418,45 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense
filter(lambda expense: expense.amount > 0, corporate_credit_card_expenses)
)

corporate_credit_card_expense_groups = _group_expenses(
corporate_credit_card_expenses, corporate_credit_card_expense_group_field, workspace_id)
if corporate_credit_card_expenses:
# Group split Credit Card Charges by `bank_transaction_id`
if (
configuration.corporate_credit_card_expenses_object == 'CREDIT CARD CHARGE' and
expense_group_settings.split_expense_grouping == 'MULTIPLE_LINE_ITEM'
):
ccc_expenses_without_bank_transaction_id = list(
filter(lambda expense: not expense.bank_transaction_id, corporate_credit_card_expenses)
)

ccc_expenses_with_bank_transaction_id = list(
filter(lambda expense: expense.bank_transaction_id, corporate_credit_card_expenses)
)

if ccc_expenses_without_bank_transaction_id:
groups_without_bank_transaction_id = _group_expenses(
ccc_expenses_without_bank_transaction_id, corporate_credit_card_expense_group_field, workspace_id
)

expense_groups.extend(groups_without_bank_transaction_id)

if ccc_expenses_with_bank_transaction_id:
split_expense_group_fields = [
field for field in corporate_credit_card_expense_group_field
if field not in ('expense_id', 'expense_number')
]
split_expense_group_fields.append('bank_transaction_id')

groups_with_bank_transaction_id = _group_expenses(
ccc_expenses_with_bank_transaction_id, split_expense_group_fields, workspace_id
)
expense_groups.extend(groups_with_bank_transaction_id)

else:
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)

expense_groups.extend(corporate_credit_card_expense_groups)
for expense_group in expense_groups:
if expense_group_settings.reimbursable_export_date_type == 'last_spent_at':
expense_group['last_spent_at'] = Expense.objects.filter(
Expand Down
76 changes: 39 additions & 37 deletions apps/netsuite/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1581,51 +1581,50 @@ def get_bill(self, internal_id):
return bill

def construct_credit_card_charge_lineitems(
self, credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping,
self, credit_card_charge_lineitems: List[CreditCardChargeLineItem], general_mapping: GeneralMapping,
attachment_links: Dict, cluster_domain: str, org_id: str) -> List[Dict]:
"""
Create credit_card_charge line items
:return: constructed line items
"""
line = credit_card_charge_lineitem

lines = []

expense = Expense.objects.get(pk=line.expense_id)

netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id)

base_line = {
'account': {'internalId': line.account_id},
'amount': line.amount,
'memo': line.memo,
'grossAmt': line.amount,
'department': {'internalId': line.department_id},
'class': {'internalId': line.class_id},
'location': {'internalId': line.location_id},
'customer': {'internalId': line.customer_id},
'customFieldList': netsuite_custom_segments,
'isBillable': line.billable,
'taxAmount': None,
'taxCode': {
'externalId': None,
'internalId': None,
'name': None,
'type': 'taxGroup'
},
}
for line in credit_card_charge_lineitems:
expense = Expense.objects.get(pk=line.expense_id)

# Handle cases where no tax is applied first
if line.tax_item_id is None or line.tax_amount is None:
lines.append(base_line)
else:
lines += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'CREDIT_CARD_CHARGE', general_mapping)
netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id)

base_line = {
'account': {'internalId': line.account_id},
'amount': line.amount,
'memo': line.memo,
'grossAmt': line.amount,
'department': {'internalId': line.department_id},
'class': {'internalId': line.class_id},
'location': {'internalId': line.location_id},
'customer': {'internalId': line.customer_id},
'customFieldList': netsuite_custom_segments,
'isBillable': line.billable,
'taxAmount': None,
'taxCode': {
'externalId': None,
'internalId': None,
'name': None,
'type': 'taxGroup'
},
}

# Handle cases where no tax is applied first
if line.tax_item_id is None or line.tax_amount is None:
lines.append(base_line)
else:
lines += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'CREDIT_CARD_CHARGE', general_mapping)

return lines

def __construct_credit_card_charge(
self, credit_card_charge: CreditCardCharge,
credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict) -> Dict:
credit_card_charge_lineitems: List[CreditCardChargeLineItem], general_mapping: GeneralMapping, attachment_links: Dict) -> Dict:
"""
Create a credit_card_charge
:return: constructed credit_card_charge
Expand Down Expand Up @@ -1664,15 +1663,15 @@ def __construct_credit_card_charge(
'memo': credit_card_charge.memo,
'tranid': credit_card_charge.reference_number,
'expenses': self.construct_credit_card_charge_lineitems(
credit_card_charge_lineitem, general_mapping, attachment_links, cluster_domain, org_id
credit_card_charge_lineitems, general_mapping, attachment_links, cluster_domain, org_id
),
'externalId': credit_card_charge.external_id
}

return credit_card_charge_payload

def post_credit_card_charge(self, credit_card_charge: CreditCardCharge,
credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict,
credit_card_charge_lineitems: List[CreditCardChargeLineItem], general_mapping: GeneralMapping, attachment_links: Dict,
refund: bool):
"""
Post vendor credit_card_charges to NetSuite
Expand All @@ -1694,12 +1693,15 @@ def post_credit_card_charge(self, credit_card_charge: CreditCardCharge,
f"script=customscript_cc_charge_fyle&deploy=customdeploy_cc_charge_fyle"

if refund:
credit_card_charge_lineitem.amount = abs(credit_card_charge_lineitem.amount)
for credit_card_charge_lineitem in credit_card_charge_lineitems:
credit_card_charge_lineitem.amount = abs(credit_card_charge_lineitem.amount)
credit_card_charge_lineitem.save()

url = f"https://{account.lower()}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?" \
f"script=customscript_cc_refund_fyle&deploy=customdeploy_cc_refund_fyle"
f"script=customscript_cc_refund_fyle&deploy=customdeploy_cc_refund_fyle"

credit_card_charges_payload = self.__construct_credit_card_charge(
credit_card_charge, credit_card_charge_lineitem, general_mapping, attachment_links)
credit_card_charge, credit_card_charge_lineitems, general_mapping, attachment_links)

logger.info("| Payload for Credit Card Charge creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} CREDIT_CARD_CHARGE_PAYLOAD: {}}}".format(self.workspace_id, credit_card_charge.expense_group.id, credit_card_charges_payload))

Expand Down
134 changes: 67 additions & 67 deletions apps/netsuite/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,103 +684,103 @@ class Meta:
db_table = 'credit_card_charge_lineitems'

@staticmethod
def create_credit_card_charge_lineitem(expense_group: ExpenseGroup, configuration: Configuration):
def create_credit_card_charge_lineitems(expense_group: ExpenseGroup, configuration: Configuration):
"""
Create credit card charge lineitems
:param expense_group: expense group
:param configuration: Workspace Configuration Settings
:return: credit card charge lineitems objects
"""
lineitem = expense_group.expenses.first()
credit_card_charge = CreditCardCharge.objects.get(expense_group=expense_group)
general_mappings = GeneralMapping.objects.get(workspace_id=expense_group.workspace_id)

credit_card_charge_lineitem_objects = []
for lineitem in expense_group.expenses.all():

category = lineitem.category if (lineitem.category == lineitem.sub_category or lineitem.sub_category == None) else '{0} / {1}'.format(
lineitem.category, lineitem.sub_category)

account = CategoryMapping.objects.filter(
source_category__value=category,
workspace_id=expense_group.workspace_id
).first()

class_id = None
if expense_group.fund_source == 'CCC' and general_mappings.use_employee_class:
employee_mapping = EmployeeMapping.objects.filter(
source_employee__value=expense_group.description.get('employee_email'),
workspace_id=expense_group.workspace_id
).first()
if employee_mapping and employee_mapping.destination_employee:
class_id = employee_mapping.destination_employee.detail.get('class_id')

if not class_id:
class_id = get_class_id_or_none(expense_group, lineitem)

department_id = get_department_id_or_none(expense_group, lineitem)
category = lineitem.category if (lineitem.category == lineitem.sub_category or lineitem.sub_category == None) else '{0} / {1}'.format(
lineitem.category, lineitem.sub_category)

if expense_group.fund_source == 'CCC' and general_mappings.use_employee_department and \
general_mappings.department_level in ('ALL', 'TRANSACTION_LINE'):
employee_mapping = EmployeeMapping.objects.filter(
source_employee__value=expense_group.description.get('employee_email'),
account = CategoryMapping.objects.filter(
source_category__value=category,
workspace_id=expense_group.workspace_id
).first()
if employee_mapping and employee_mapping.destination_employee:
if employee_mapping.destination_employee.detail.get('department_id'):
department_id = employee_mapping.destination_employee.detail.get('department_id')

if not department_id:
if general_mappings.department_id and general_mappings.department_level in ['TRANSACTION_LINE', 'ALL']:
department_id = general_mappings.department_id
class_id = None
if expense_group.fund_source == 'CCC' and general_mappings.use_employee_class:
employee_mapping = EmployeeMapping.objects.filter(
source_employee__value=expense_group.description.get('employee_email'),
workspace_id=expense_group.workspace_id
).first()
if employee_mapping and employee_mapping.destination_employee:
class_id = employee_mapping.destination_employee.detail.get('class_id')

if not class_id:
class_id = get_class_id_or_none(expense_group, lineitem)

location_id = get_location_id_or_none(expense_group, lineitem)
department_id = get_department_id_or_none(expense_group, lineitem)

if expense_group.fund_source == 'CCC' and general_mappings.use_employee_location and\
general_mappings.location_level in ('ALL', 'TRANSACTION_LINE'):
if expense_group.fund_source == 'CCC' and general_mappings.use_employee_department and \
general_mappings.department_level in ('ALL', 'TRANSACTION_LINE'):
employee_mapping = EmployeeMapping.objects.filter(
source_employee__value=expense_group.description.get('employee_email'),
workspace_id=expense_group.workspace_id
).first()
if employee_mapping and employee_mapping.destination_employee:
location_id = employee_mapping.destination_employee.detail.get('location_id')
if employee_mapping.destination_employee.detail.get('department_id'):
department_id = employee_mapping.destination_employee.detail.get('department_id')

if not location_id:
if general_mappings.location_id and general_mappings.location_level in ['TRANSACTION_LINE', 'ALL']:
location_id = general_mappings.location_id
if not department_id:
if general_mappings.department_id and general_mappings.department_level in ['TRANSACTION_LINE', 'ALL']:
department_id = general_mappings.department_id

location_id = get_location_id_or_none(expense_group, lineitem)

if expense_group.fund_source == 'CCC' and general_mappings.use_employee_location and\
general_mappings.location_level in ('ALL', 'TRANSACTION_LINE'):
employee_mapping = EmployeeMapping.objects.filter(
source_employee__value=expense_group.description.get('employee_email'),
workspace_id=expense_group.workspace_id
).first()
if employee_mapping and employee_mapping.destination_employee:
location_id = employee_mapping.destination_employee.detail.get('location_id')

if not location_id:
if general_mappings.location_id and general_mappings.location_level in ['TRANSACTION_LINE', 'ALL']:
location_id = general_mappings.location_id

custom_segments = get_custom_segments(expense_group, lineitem)
custom_segments = get_custom_segments(expense_group, lineitem)

customer_id = get_customer_id_or_none(expense_group, lineitem)
customer_id = get_customer_id_or_none(expense_group, lineitem)

billable = lineitem.billable
if customer_id:
if not billable:
billable = lineitem.billable
if customer_id:
if not billable:
billable = False
else:
billable = False
else:
billable = False

credit_card_charge_lineitem_object, _ = CreditCardChargeLineItem.objects.update_or_create(
credit_card_charge=credit_card_charge,
expense_id=lineitem.id,
defaults={
'account_id': account.destination_account.destination_id \
if account and account.destination_account else None,
'location_id': location_id,
'class_id': class_id,
'department_id': department_id,
'customer_id': customer_id,
'amount': lineitem.amount,
'tax_item_id': get_tax_item_id_or_none(expense_group, general_mappings,lineitem),
'tax_amount': lineitem.tax_amount,
'billable': billable,
'memo': get_expense_purpose(lineitem, category, configuration),
'netsuite_custom_segments': custom_segments
}
)
credit_card_charge_lineitem_object, _ = CreditCardChargeLineItem.objects.update_or_create(
credit_card_charge=credit_card_charge,
expense_id=lineitem.id,
defaults={
'account_id': account.destination_account.destination_id \
if account and account.destination_account else None,
'location_id': location_id,
'class_id': class_id,
'department_id': department_id,
'customer_id': customer_id,
'amount': lineitem.amount,
'tax_item_id': get_tax_item_id_or_none(expense_group, general_mappings,lineitem),
'tax_amount': lineitem.tax_amount,
'billable': billable,
'memo': get_expense_purpose(lineitem, category, configuration),
'netsuite_custom_segments': custom_segments
}
)

credit_card_charge_lineitem_objects.append(credit_card_charge_lineitem_object)
credit_card_charge_lineitem_objects.append(credit_card_charge_lineitem_object)

return credit_card_charge_lineitem_object
return credit_card_charge_lineitem_objects


class ExpenseReport(models.Model):
Expand Down
Loading
Loading