diff --git a/apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py b/apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py new file mode 100644 index 00000000..242be4bb --- /dev/null +++ b/apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2024-11-04 10:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mappings', '0014_auto_20240417_0807'), + ] + + operations = [ + migrations.AddField( + model_name='generalmapping', + name='is_tax_balancing_enabled', + field=models.BooleanField(default=False, help_text='Is tax balancing enabled'), + ), + ] diff --git a/apps/mappings/models.py b/apps/mappings/models.py index 6c9f4346..a8717d79 100644 --- a/apps/mappings/models.py +++ b/apps/mappings/models.py @@ -68,6 +68,7 @@ class GeneralMapping(models.Model): ) override_tax_details = models.BooleanField(default=False, help_text='Override tax details') + is_tax_balancing_enabled = models.BooleanField(default=False, help_text='Is tax balancing enabled') workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model', related_name='general_mappings') created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') diff --git a/apps/netsuite/connector.py b/apps/netsuite/connector.py index 4f273ecc..d3baa9f5 100644 --- a/apps/netsuite/connector.py +++ b/apps/netsuite/connector.py @@ -1,5 +1,6 @@ import re import json +import copy from datetime import datetime, timedelta from django.utils import timezone @@ -1161,42 +1162,100 @@ def sync_customers(self): attributes, 'PROJECT', self.workspace_id, True) return [] - - def construct_bill_lineitems( - self, - bill_lineitems: List[BillLineitem], - attachment_links: Dict, - cluster_domain: str, org_id: str, - override_tax_details: bool - ) -> List[Dict]: + + def handle_taxed_line_items(self, base_line, line, workspace_id, export_module, general_mapping: GeneralMapping): """ - Create bill line items - :return: constructed line items + Handle line items where tax is applied or modified by the user. + :param base_line: The base line item template that will be modified. + :param line: The original line with tax and amount information. + :param is_credit_card_charge: Boolean flag to differentiate between credit card charges and other transactions. + :return: List of lines (taxed and/or untaxed). """ - expense_list = [] - item_list = [] + tax_item = DestinationAttribute.objects.filter( + workspace_id=workspace_id, + attribute_type='TAX_ITEM', + destination_id=str(line.tax_item_id) + ).first() + tax_item_rate = tax_item.detail['tax_rate'] - for line in bill_lineitems: - expense: Expense = Expense.objects.get(pk=line.expense_id) + lines = [] + original_amount = round(line.amount, 2) + expected_tax_amount = round((line.amount * (tax_item_rate / 100)) / (1 + (tax_item_rate / 100)), 2) + + if general_mapping.is_tax_balancing_enabled and round(line.tax_amount, 2) != expected_tax_amount: + # Recalculate the net amount based on the modified tax + recalculated_net_amount = round((line.tax_amount * 100) / tax_item_rate, 2) + untaxed_amount = round(original_amount - recalculated_net_amount - line.tax_amount, 2) + + # Create a taxable line item + taxable_line = copy.deepcopy(base_line) + taxable_line['amount'] = recalculated_net_amount + taxable_line['taxCode']['internalId'] = line.tax_item_id + + # Create an untaxed line item + untaxed_line = copy.deepcopy(base_line) + untaxed_line['amount'] = untaxed_amount + untaxed_line['taxCode']['internalId'] = general_mapping.default_tax_code_id # Use default for untaxed items + + if export_module == 'JOURNAL_ENTRY': + taxable_line['grossAmt'] = round(recalculated_net_amount + line.tax_amount, 2) + taxable_line['debit'] = recalculated_net_amount + taxable_line.pop('amount', None) + untaxed_line['grossAmt'] = round(untaxed_amount, 2) + untaxed_line['debit'] = untaxed_amount + untaxed_line.pop('amount', None) + + if export_module in ('EXPENSE_REPORT', 'JOURNAL_ENTRY'): + taxable_line['tax1Amt'] = round(line.tax_amount, 2) # Tax is applied to this line - netsuite_custom_segments = line.netsuite_custom_segments + if export_module == 'BILL' and taxable_line.get('rate'): + taxable_line['rate'] = str(round(line.amount - line.tax_amount, 2)) - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) + lines.append(taxable_line) + lines.append(untaxed_line) + else: + # When the tax is not modified, just subtract the tax and apply it directly + base_line['amount'] = round(original_amount - line.tax_amount, 2) + base_line['taxCode']['internalId'] = line.tax_item_id + + if export_module in ('EXPENSE_REPORT', 'JOURNAL_ENTRY'): + base_line['tax1Amt'] = round(line.tax_amount, 2) # Tax is applied to this line + + if export_module == 'BILL' and base_line.get('rate'): + base_line['rate'] = str(round(line.amount - line.tax_amount, 2)) + if export_module == 'JOURNAL_ENTRY': + base_line['grossAmt'] = original_amount + base_line['debit'] = round(original_amount - line.tax_amount, 2) + base_line.pop('amount', None) + + lines.append(base_line) + + return lines + + def prepare_custom_segments(self, line_netsuite_custom_segments, attachment_links, expense, org_id, is_credit=False): + """ + Prepare custom segments for line items. + """ + netsuite_custom_segments = line_netsuite_custom_segments + + if attachment_links and expense.expense_id in attachment_links: + netsuite_custom_segments.append( + { + 'scriptId': 'custcolfyle_receipt_link', + 'type': 'String', + 'value': attachment_links[expense.expense_id] + } + ) + netsuite_custom_segments.append( + { + 'scriptId': 'custcolfyle_receipt_link_2', + 'type': 'String', + 'value': attachment_links[expense.expense_id] + } + ) + + if not is_credit: netsuite_custom_segments.append( { 'scriptId': 'custcolfyle_expense_url', @@ -1220,13 +1279,35 @@ def construct_bill_lineitems( } ) - lineitem = { + return netsuite_custom_segments + + def construct_bill_lineitems( + self, + bill_lineitems: List[BillLineitem], + attachment_links: Dict, + cluster_domain: str, org_id: str, + override_tax_details: bool, + general_mapping: GeneralMapping + ) -> List[Dict]: + """ + Create bill line items + :return: constructed line items + """ + expense_list = [] + item_list = [] + + for line in bill_lineitems: + expense: 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 = { 'orderDoc': None, 'orderLine': None, 'line': None, - 'amount': line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount, - 'grossAmt': None if override_tax_details else line.amount, - 'taxDetailsReference': expense.expense_number if override_tax_details else None, + 'amount': line.amount, + 'grossAmt': line.amount, + 'taxDetailsReference': None, 'department': { 'name': None, 'internalId': line.department_id, @@ -1254,10 +1335,10 @@ def construct_bill_lineitems( 'customFieldList': netsuite_custom_segments, 'isBillable': line.billable, 'tax1Amt': None, - 'taxAmount': line.tax_amount if (line.tax_item_id and line.tax_amount is not None and not override_tax_details) else None, + 'taxAmount': None, 'taxCode':{ 'name': None, - 'internalId': line.tax_item_id if (line.tax_item_id and line.tax_amount is not None and not override_tax_details) else None, + 'internalId': None, 'externalId': None, 'type': 'taxGroup' }, @@ -1270,41 +1351,59 @@ def construct_bill_lineitems( } if line.detail_type == 'AccountBasedExpenseLineDetail': - lineitem['account'] = { + base_line['account'] = { 'name': None, 'internalId': line.account_id, 'externalId': None, 'type': 'account' } - lineitem['category'] = None - lineitem['memo'] = line.memo - lineitem['projectTask'] = None + base_line['category'] = None + base_line['memo'] = line.memo + base_line['projectTask'] = None - expense_list.append(lineitem) + if line.tax_item_id is None or line.tax_amount is None: + expense_list.append(base_line) + else: + if override_tax_details: + base_line['grossAmt'] = None + base_line['taxDetailsReference'] = expense.expense_number + base_line['amount'] = line.amount - line.tax_amount + expense_list.append(base_line) + else: + expense_list += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'BILL', general_mapping) else: - lineitem['item'] = { + base_line['item'] = { 'name': None, 'internalId': line.item_id, 'externalId': None, 'type': None } - lineitem['vendorName'] = None - lineitem['quantity'] = 1.0 - lineitem['units'] = None - lineitem['inventoryDetail'] = None - lineitem['description'] = line.memo - lineitem['serialNumbers'] = None - lineitem['binNumbers'] = None - lineitem['expirationDate'] = None - lineitem['rate'] = str(line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount) - lineitem['options'] = None - lineitem['landedCostCategory'] = None - lineitem['billVarianceStatus'] = None - lineitem['billreceiptsList'] = None - lineitem['landedCost'] = None - - item_list.append(lineitem) + base_line['vendorName'] = None + base_line['quantity'] = 1.0 + base_line['units'] = None + base_line['inventoryDetail'] = None + base_line['description'] = line.memo + base_line['serialNumbers'] = None + base_line['binNumbers'] = None + base_line['expirationDate'] = None + base_line['rate'] = str(line.amount) + base_line['options'] = None + base_line['landedCostCategory'] = None + base_line['billVarianceStatus'] = None + base_line['billreceiptsList'] = None + base_line['landedCost'] = None + + if line.tax_item_id is None or line.tax_amount is None: + item_list.append(base_line) + else: + if override_tax_details: + base_line['grossAmt'] = None + base_line['taxDetailsReference'] = expense.expense_number + base_line['amount'] = line.amount - line.tax_amount + item_list.append(base_line) + else: + item_list += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'BILL', general_mapping) return expense_list, item_list @@ -1339,7 +1438,7 @@ def construct_tax_details_list(self, bill_lineitems: List[BillLineitem]): return tax_details_list - def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]) -> Dict: + def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem], general_mappings: GeneralMapping) -> Dict: """ Create a bill :return: constructed bill @@ -1351,7 +1450,7 @@ def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]) -> Di org_id = Workspace.objects.get(id=bill.expense_group.workspace_id).fyle_org_id tax_details_list = None - expense_list, item_list = self.construct_bill_lineitems(bill_lineitems, {}, cluster_domain, org_id, bill.override_tax_details) + expense_list, item_list = self.construct_bill_lineitems(bill_lineitems, {}, cluster_domain, org_id, bill.override_tax_details, general_mappings) if bill.override_tax_details: tax_details_list = self.construct_tax_details_list(bill_lineitems) @@ -1447,13 +1546,13 @@ def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]) -> Di return bill_payload - def post_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]): + def post_bill(self, bill: Bill, bill_lineitems: List[BillLineitem], general_mappings: GeneralMapping): """ Post vendor bills to NetSuite """ configuration = Configuration.objects.get(workspace_id=self.workspace_id) try: - bills_payload = self.__construct_bill(bill, bill_lineitems) + bills_payload = self.__construct_bill(bill, bill_lineitems, general_mappings) logger.info("| Payload for Bill creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} BILL_PAYLOAD: {}}}".format(self.workspace_id, bill.expense_group.id, bills_payload)) created_bill = self.connection.vendor_bills.post(bills_payload) @@ -1465,7 +1564,7 @@ def post_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]): message = 'An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.' if configuration.change_accounting_period and detail['message'] == message: first_day_of_month = datetime.today().date().replace(day=1) - bills_payload = self.__construct_bill(bill, bill_lineitems) + bills_payload = self.__construct_bill(bill, bill_lineitems, general_mappings) bills_payload['tranDate'] = first_day_of_month created_bill = self.connection.vendor_bills.post(bills_payload) @@ -1482,7 +1581,7 @@ def get_bill(self, internal_id): return bill def construct_credit_card_charge_lineitems( - self, credit_card_charge_lineitem: CreditCardChargeLineItem, + self, credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict, cluster_domain: str, org_id: str) -> List[Dict]: """ Create credit_card_charge line items @@ -1494,80 +1593,39 @@ def construct_credit_card_charge_lineitems( expense = Expense.objects.get(pk=line.expense_id) - netsuite_custom_segments = line.netsuite_custom_segments + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id) - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url_2', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - - line = { - 'account': { - 'internalId': line.account_id - }, - 'amount': line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount, + 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 - }, + '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': { - 'name': None, - 'internalId': line.tax_item_id if (line.tax_item_id and line.tax_amount is not None) else None, 'externalId': None, + 'internalId': None, + 'name': None, 'type': 'taxGroup' }, } - lines.append(line) + + # 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, attachment_links: Dict) -> Dict: + credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict) -> Dict: """ Create a credit_card_charge :return: constructed credit_card_charge @@ -1606,7 +1664,7 @@ 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, attachment_links, cluster_domain, org_id + credit_card_charge_lineitem, general_mapping, attachment_links, cluster_domain, org_id ), 'externalId': credit_card_charge.external_id } @@ -1614,7 +1672,7 @@ def __construct_credit_card_charge( return credit_card_charge_payload def post_credit_card_charge(self, credit_card_charge: CreditCardCharge, - credit_card_charge_lineitem: CreditCardChargeLineItem, attachment_links: Dict, + credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict, refund: bool): """ Post vendor credit_card_charges to NetSuite @@ -1641,7 +1699,7 @@ def post_credit_card_charge(self, credit_card_charge: CreditCardCharge, 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, attachment_links) + credit_card_charge, credit_card_charge_lineitem, 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)) @@ -1696,7 +1754,7 @@ def post_credit_card_charge(self, credit_card_charge: CreditCardCharge, raise NetSuiteRequestError(code=code, message=message) def construct_expense_report_lineitems( - self, expense_report_lineitems: List[ExpenseReportLineItem], attachment_links: Dict, cluster_domain: str, + self, expense_report_lineitems: List[ExpenseReportLineItem], general_mapping: GeneralMapping, attachment_links: Dict, cluster_domain: str, org_id: str ) -> List[Dict]: """ @@ -1707,7 +1765,7 @@ def construct_expense_report_lineitems( for line in expense_report_lineitems: expense: Expense = Expense.objects.get(pk=line.expense_id) - netsuite_custom_segments = line.netsuite_custom_segments + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id) if expense.foreign_amount: if expense.amount == 0: @@ -1717,47 +1775,8 @@ def construct_expense_report_lineitems( else: foreign_amount = None - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url_2', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - - lineitem = { - 'amount': line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount, + base_line = { + 'amount': line.amount, 'category': { 'name': None, 'internalId': line.category, @@ -1809,10 +1828,10 @@ def construct_expense_report_lineitems( 'rate': None, 'receipt': None, 'refNumber': None, - 'tax1Amt': line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else None, + 'tax1Amt': None, 'taxCode': { 'name': None, - 'internalId': line.tax_item_id if (line.tax_item_id and line.tax_amount is not None) else None, + 'internalId': None, 'externalId': None, 'type': 'taxGroup' }, @@ -1820,12 +1839,16 @@ def construct_expense_report_lineitems( 'taxRate2': None } - lines.append(lineitem) + # 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, 'EXPENSE_REPORT', general_mapping) return lines def __construct_expense_report(self, expense_report: ExpenseReport, - expense_report_lineitems: List[ExpenseReportLineItem]) -> Dict: + expense_report_lineitems: List[ExpenseReportLineItem], general_mapping: GeneralMapping) -> Dict: """ Create a expense report :return: constructed expense report @@ -1909,7 +1932,7 @@ def __construct_expense_report(self, expense_report: ExpenseReport, 'type': 'location' }, 'expenseList': self.construct_expense_report_lineitems( - expense_report_lineitems, {}, cluster_domain, org_id + expense_report_lineitems, general_mapping, {}, cluster_domain, org_id ), 'accountingBookDetailList': None, 'customFieldList': None, @@ -1921,14 +1944,14 @@ def __construct_expense_report(self, expense_report: ExpenseReport, def post_expense_report( self, expense_report: ExpenseReport, - expense_report_lineitems: List[ExpenseReportLineItem]): + expense_report_lineitems: List[ExpenseReportLineItem], general_mapping: GeneralMapping): """ Post expense reports to NetSuite """ configuration = Configuration.objects.get(workspace_id=self.workspace_id) try: expense_report_payload = self.__construct_expense_report(expense_report, - expense_report_lineitems) + expense_report_lineitems, general_mapping) logger.info("| Payload for Expense Report creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} EXPENSE_REPORT_PAYLOAD: {}}}".format(self.workspace_id, expense_report.expense_group.id, expense_report_payload)) @@ -1942,7 +1965,7 @@ def post_expense_report( if configuration.change_accounting_period and detail['message'] == message: expense_report_payload = self.__construct_expense_report(expense_report, - expense_report_lineitems) + expense_report_lineitems, general_mapping) first_day_of_month = datetime.today().date().replace(day=1) expense_report_payload['tranDate'] = first_day_of_month.strftime('%Y-%m-%dT%H:%M:%S') @@ -1962,7 +1985,7 @@ def get_expense_report(self, internal_id): return expense_report - def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[JournalEntryLineItem], org_id: str, + def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[JournalEntryLineItem], general_mapping: GeneralMapping, org_id: str, credit=None, debit=None, attachment_links: Dict = None, cluster_domain: str = None) -> List[Dict]: """ @@ -1980,50 +2003,9 @@ def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[Journa if debit is None: account_ref = line.debit_account_id - netsuite_custom_segments = line.netsuite_custom_segments - - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - - if debit: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url_2', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id, credit) - tax_inclusive_amount = round((line.amount - line.tax_amount), 2) if (line.tax_amount is not None and line.tax_item_id ) else line.amount - lineitem = { + base_line = { 'account': { 'name': None, 'internalId': account_ref, @@ -2054,14 +2036,14 @@ def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[Journa 'externalId': None, 'type': 'vendor' }, - 'credit': line.amount if credit is not None else None, + 'credit': None, 'creditTax': None, 'customFieldList': netsuite_custom_segments, - 'debit': tax_inclusive_amount if debit is not None else None, + 'debit': line.amount, 'debitTax': None, 'eliminate': None, 'endDate': None, - 'grossAmt': line.amount if (line.tax_amount is not None and line.tax_item_id and debit is not None) else None, + 'grossAmt': None, 'line': None, 'lineTaxCode': None, 'lineTaxRate': None, @@ -2074,18 +2056,28 @@ def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[Journa 'tax1Acct': None, 'taxAccount': None, 'taxBasis': None, - 'tax1Amt': line.tax_amount if (line.tax_amount is not None and line.tax_item_id and debit is not None) else None, + 'tax1Amt': None, 'taxCode': { 'name': None, - 'internalId': line.tax_item_id if (line.tax_amount is not None and line.tax_item_id ) else None, + 'internalId': None, 'externalId': None, 'type': 'taxGroup' - } if debit is not None else None, + }, 'taxRate1': None, 'totalAmount': None, } - lines.append(lineitem) + + if debit: + 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, 'JOURNAL_ENTRY', general_mapping) + elif credit: + base_line['credit'] = line.amount + base_line['debit'] = None + lines.append(base_line) + return lines @@ -2172,7 +2164,7 @@ def __construct_single_itemized_credit_line(journal_entry_lineitems: List[Journa return lines def __construct_journal_entry(self, journal_entry: JournalEntry, - journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration) -> Dict: + journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration, general_mapping: GeneralMapping) -> Dict: """ Create a journal entry report :return: constructed journal entry @@ -2185,12 +2177,12 @@ def __construct_journal_entry(self, journal_entry: JournalEntry, if configuration.je_single_credit_line: credit_line = self.__construct_single_itemized_credit_line(journal_entry_lineitems) else: - credit_line = self.construct_journal_entry_lineitems(journal_entry_lineitems, credit='Credit', org_id=org_id) + credit_line = self.construct_journal_entry_lineitems(journal_entry_lineitems, credit='Credit', org_id=org_id, general_mapping=general_mapping) debit_line = self.construct_journal_entry_lineitems( journal_entry_lineitems, debit='Debit', attachment_links={}, - cluster_domain=cluster_domain, org_id=org_id + cluster_domain=cluster_domain, org_id=org_id, general_mapping=general_mapping ) lines = [] lines.extend(credit_line) @@ -2256,13 +2248,13 @@ def __construct_journal_entry(self, journal_entry: JournalEntry, return journal_entry_payload def post_journal_entry(self, journal_entry: JournalEntry, - journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration): + journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration, general_mapping: GeneralMapping): """ Post journal entries to NetSuite """ configuration = Configuration.objects.get(workspace_id=self.workspace_id) try: - journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration) + journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration, general_mapping) logger.info("| Payload for Journal Entry creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} JOURNAL_ENTRY_PAYLOAD: {}}}".format(self.workspace_id, journal_entry.expense_group.id, journal_entry_payload)) @@ -2276,7 +2268,7 @@ def post_journal_entry(self, journal_entry: JournalEntry, if configuration.change_accounting_period and detail['message'] == message: first_day_of_month = datetime.today().date().replace(day=1) - journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration) + journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration, general_mapping) journal_entry_payload['tranDate'] = first_day_of_month created_journal_entry = self.connection.journal_entries.post(journal_entry_payload) diff --git a/apps/netsuite/models.py b/apps/netsuite/models.py index b5c104a4..d69657b4 100644 --- a/apps/netsuite/models.py +++ b/apps/netsuite/models.py @@ -115,12 +115,18 @@ def get_tax_group_mapping(lineitem: Expense = None, workspace_id: int = None): def get_tax_item_id_or_none(expense_group: ExpenseGroup, general_mapping: GeneralMapping, lineitem: Expense = None): tax_code = None - mapping = get_tax_group_mapping(lineitem, expense_group.workspace_id) + tax_setting: MappingSetting = MappingSetting.objects.filter( + workspace_id=expense_group.workspace_id, + destination_field='TAX_ITEM' + ).first() + + if tax_setting: + mapping = get_tax_group_mapping(lineitem, expense_group.workspace_id) - if mapping: - tax_code = mapping.destination.destination_id - else: - tax_code = general_mapping.default_tax_code_id + if mapping: + tax_code = mapping.destination.destination_id + else: + tax_code = general_mapping.default_tax_code_id return tax_code diff --git a/apps/netsuite/tasks.py b/apps/netsuite/tasks.py index 36f7dfd3..da56208a 100644 --- a/apps/netsuite/tasks.py +++ b/apps/netsuite/tasks.py @@ -326,9 +326,9 @@ def construct_payload_and_update_export(expense_id_receipt_url_map: dict, task_l construct_lines = getattr(netsuite_connection, func) # calling the target construct payload function with credit and debit - credit_line = construct_lines(export_line_items, credit='Credit', org_id=workspace.fyle_org_id) + credit_line = construct_lines(export_line_items, general_mappings, credit='Credit', org_id=workspace.fyle_org_id) debit_line = construct_lines( - export_line_items, debit='Debit', attachment_links=expense_id_receipt_url_map, + export_line_items, general_mappings, debit='Debit', attachment_links=expense_id_receipt_url_map, cluster_domain=cluster_domain, org_id=workspace.fyle_org_id ) lines.extend(credit_line) @@ -337,11 +337,11 @@ def construct_payload_and_update_export(expense_id_receipt_url_map: dict, task_l elif task_log.type == 'CREATING_BILL': construct_lines = getattr(netsuite_connection, func) # calling the target construct payload function - expense_list, item_list = construct_lines(export_line_items, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id, general_mappings.override_tax_details) + expense_list, item_list = construct_lines(export_line_items, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id, general_mappings.override_tax_details, general_mappings) else: construct_lines = getattr(netsuite_connection, func) # calling the target construct payload function - lines = construct_lines(export_line_items, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id) + lines = construct_lines(export_line_items, general_mappings, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id) # final payload to be sent to netsuite, since this is an update operation, we need to pass the external id if task_log.type == 'CREATING_BILL': @@ -487,7 +487,7 @@ def create_bill(expense_group: ExpenseGroup, task_log_id, last_export): bill_lineitems_objects = BillLineitem.create_bill_lineitems(expense_group, configuration) - created_bill = netsuite_connection.post_bill(bill_object, bill_lineitems_objects) + created_bill = netsuite_connection.post_bill(bill_object, bill_lineitems_objects, general_mappings) logger.info('Created Bill with Expense Group %s successfully', expense_group.id) task_log.detail = created_bill @@ -567,7 +567,7 @@ def create_credit_card_charge(expense_group, task_log_id, last_export): attachment_links[expense.expense_id] = attachment_link created_credit_card_charge = netsuite_connection.post_credit_card_charge( - credit_card_charge_object, credit_card_charge_lineitems_object, attachment_links, refund + credit_card_charge_object, credit_card_charge_lineitems_object, general_mappings, attachment_links, refund ) worker_logger.info('Created Credit Card Charge with Expense Group %s successfully', expense_group.id) @@ -612,6 +612,7 @@ def create_expense_report(expense_group, task_log_id, last_export): return configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=expense_group.workspace_id) fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) @@ -633,7 +634,7 @@ def create_expense_report(expense_group, task_log_id, last_export): ) created_expense_report = netsuite_connection.post_expense_report( - expense_report_object, expense_report_lineitems_objects + expense_report_object, expense_report_lineitems_objects, general_mapping ) worker_logger.info('Created Expense Report with Expense Group %s successfully', expense_group.id) @@ -676,6 +677,7 @@ def create_journal_entry(expense_group, task_log_id, last_export): return configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=expense_group.workspace_id) fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) @@ -698,7 +700,7 @@ def create_journal_entry(expense_group, task_log_id, last_export): ) created_journal_entry = netsuite_connection.post_journal_entry( - journal_entry_object, journal_entry_lineitems_objects, configuration + journal_entry_object, journal_entry_lineitems_objects, configuration, general_mapping ) worker_logger.info('Created Journal Entry with Expense Group %s successfully', expense_group.id) diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 55889c0f..285dd66b 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -1440,7 +1440,8 @@ CREATE TABLE public.general_mappings ( class_level character varying(255), class_name character varying(255), default_tax_code_id character varying(255), - default_tax_code_name character varying(255) + default_tax_code_name character varying(255), + is_tax_balancing_enabled boolean NOT NULL ); @@ -7987,9 +7988,10 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 198 netsuite 0027_auto_20240924_0820 2024-09-24 08:24:35.223017+00 199 fyle_accounting_mappings 0026_destinationattribute_code 2024-10-01 08:54:06.770864+00 200 workspaces 0039_configuration_je_single_credit_line 2024-10-11 13:43:49.169823+00 -201 fyle 0034_expense_is_posted_at_null 2024-11-17 20:37:53.17847+00 -202 tasks 0012_alter_tasklog_expense_group 2024-11-17 20:37:53.213044+00 -203 workspaces 0040_alter_configuration_change_accounting_period 2024-11-18 04:28:36.094429+00 +201 mappings 0015_generalmapping_is_tax_balancing_enabled 2024-11-11 18:30:21.068097+00 +202 fyle 0034_expense_is_posted_at_null 2024-11-17 20:37:53.17847+00 +203 tasks 0012_alter_tasklog_expense_group 2024-11-17 20:37:53.213044+00 +204 workspaces 0040_alter_configuration_change_accounting_period 2024-11-18 04:28:36.094429+00 \. @@ -11655,10 +11657,10 @@ COPY public.fyle_credentials (id, refresh_token, created_at, updated_at, workspa -- Data for Name: general_mappings; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.general_mappings (id, location_name, location_id, accounts_payable_name, accounts_payable_id, created_at, updated_at, workspace_id, default_ccc_account_id, default_ccc_account_name, reimbursable_account_id, reimbursable_account_name, default_ccc_vendor_id, default_ccc_vendor_name, vendor_payment_account_id, vendor_payment_account_name, location_level, department_level, use_employee_department, use_employee_class, use_employee_location, department_id, department_name, override_tax_details, class_id, class_level, class_name, default_tax_code_id, default_tax_code_name) FROM stdin; -1 hubajuba 8 Accounts Payable 25 2021-11-15 08:56:31.432106+00 2021-11-15 13:21:26.113427+00 1 \N \N 118 Unapproved Expense Reports 1674 Ashwin Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N -2 \N \N Accounts Payable 25 2021-11-16 04:18:39.195287+00 2021-11-16 04:18:39.195312+00 2 228 Aus Account 118 Unapproved Expense Reports 12104 Nilesh Aus Vendor \N \N \N \N f f f \N \N f \N \N \N \N \N -3 hukiju 10 \N \N 2021-12-03 11:24:17.962764+00 2021-12-03 11:24:17.962809+00 49 228 Aus Account 228 Aus Account 12104 Nilesh Aus Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N +COPY public.general_mappings (id, location_name, location_id, accounts_payable_name, accounts_payable_id, created_at, updated_at, workspace_id, default_ccc_account_id, default_ccc_account_name, reimbursable_account_id, reimbursable_account_name, default_ccc_vendor_id, default_ccc_vendor_name, vendor_payment_account_id, vendor_payment_account_name, location_level, department_level, use_employee_department, use_employee_class, use_employee_location, department_id, department_name, override_tax_details, class_id, class_level, class_name, default_tax_code_id, default_tax_code_name, is_tax_balancing_enabled) FROM stdin; +1 hubajuba 8 Accounts Payable 25 2021-11-15 08:56:31.432106+00 2021-11-15 13:21:26.113427+00 1 \N \N 118 Unapproved Expense Reports 1674 Ashwin Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N f +2 \N \N Accounts Payable 25 2021-11-16 04:18:39.195287+00 2021-11-16 04:18:39.195312+00 2 228 Aus Account 118 Unapproved Expense Reports 12104 Nilesh Aus Vendor \N \N \N \N f f f \N \N f \N \N \N \N \N f +3 hukiju 10 \N \N 2021-12-03 11:24:17.962764+00 2021-12-03 11:24:17.962809+00 49 228 Aus Account 228 Aus Account 12104 Nilesh Aus Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N f \. @@ -11901,6 +11903,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 47, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- +SELECT pg_catalog.setval('public.django_migrations_id_seq', 201, true); SELECT pg_catalog.setval('public.django_migrations_id_seq', 203, true); diff --git a/tests/test_netsuite/conftest.py b/tests/test_netsuite/conftest.py index 9c0f4dbc..6995201e 100644 --- a/tests/test_netsuite/conftest.py +++ b/tests/test_netsuite/conftest.py @@ -78,6 +78,9 @@ def create_expense_report(db, add_netsuite_credentials, add_fyle_credentials): ) expense_group = ExpenseGroup.objects.filter(workspace_id=1).first() + for expense in expense_group.expenses.all(): + expense.workspace_id = 1 + expense.save() configuration = Configuration.objects.get(workspace_id=1) expense_report = ExpenseReport.create_expense_report(expense_group) expense_report_lineitems = ExpenseReportLineItem.create_expense_report_lineitems(expense_group, configuration) @@ -89,6 +92,9 @@ def create_expense_report(db, add_netsuite_credentials, add_fyle_credentials): def create_bill_account_based(db, add_netsuite_credentials, add_fyle_credentials): expense_group = ExpenseGroup.objects.get(id=2) + for expense in expense_group.expenses.all(): + expense.workspace_id = 1 + expense.save() configuration = Configuration.objects.get(workspace_id=1) bill = Bill.create_bill(expense_group) bill_lineitem = BillLineitem.create_bill_lineitems(expense_group, configuration) @@ -243,6 +249,9 @@ def create_bill_task(db, add_netsuite_credentials, add_fyle_credentials): def create_journal_entry(db, add_netsuite_credentials, add_fyle_credentials): expense_group = ExpenseGroup.objects.filter(workspace_id=49).first() + for expense in expense_group.expenses.all(): + expense.workspace_id = 1 + expense.save() configuration = Configuration.objects.get(workspace_id=1) journal_entry = JournalEntry.create_journal_entry(expense_group) journal_entry_lineitem = JournalEntryLineItem.create_journal_entry_lineitems(expense_group, configuration) @@ -254,6 +263,9 @@ def create_journal_entry(db, add_netsuite_credentials, add_fyle_credentials): def create_credit_card_charge(db, add_netsuite_credentials, add_fyle_credentials): expense_group = ExpenseGroup.objects.filter(workspace_id=49).last() + for expense in expense_group.expenses.all(): + expense.workspace_id = 49 + expense.save() configuration = Configuration.objects.get(workspace_id=1) credit_card_charge_object = CreditCardCharge.create_credit_card_charge(expense_group) @@ -292,3 +304,21 @@ def add_custom_segment(db): custom_segments[i] = CustomSegment(**custom_segments[i]) CustomSegment.objects.bulk_create(custom_segments) + +@pytest.fixture +def add_tax_destination_attributes(db): + + for i in(1, 2, 49): + DestinationAttribute.objects.create( + id = 98765+i, + attribute_type='TAX_ITEM', + value='Rushikesh', + destination_id = '103578', + active = True, + detail = { + 'tax_rate': 5.0 + }, + workspace_id = i, + created_at = datetime.now(), + updated_at = datetime.now(), + ) \ No newline at end of file diff --git a/tests/test_netsuite/fixtures.py b/tests/test_netsuite/fixtures.py index 566486c6..0dbb4884 100644 --- a/tests/test_netsuite/fixtures.py +++ b/tests/test_netsuite/fixtures.py @@ -2,6 +2,33 @@ from datetime import datetime, timezone data = { + 'tax_list_detail' : { + 'taxDetails': [ + { + 'taxType': { + 'internalId': 'tax_type_1' + }, + 'taxCode': { + 'internalId': 'tax_code_1' + }, + 'taxRate': 'tax_type_1', + 'taxBasis': 90.0, + 'taxAmount': 10.0, + 'taxDetailsReference': 'EXP001' + }, + { + 'taxType': { + 'internalId': 'tax_type_1' + }, + 'taxCode': { + 'internalId': 'tax_code_1' + }, + 'taxRate': 'tax_type_1', 'taxBasis': 180.0, + 'taxAmount': 20.0, + 'taxDetailsReference': 'EXP002' + } + ] + }, 'expense':[{ "id": 1, "employee_email": "ashwin.t@fyle.in", @@ -964,7 +991,12 @@ 'taxAccount': None, 'taxBasis': None, 'tax1Amt': None, - 'taxCode': None, + 'taxCode': { + 'externalId': None, + 'internalId': None, + 'name': None, + 'type': 'taxGroup' + }, 'taxRate1': None, 'totalAmount': None, }, { @@ -1170,17 +1202,19 @@ 'location': {'internalId': None}, 'customer': {'internalId': None}, 'customFieldList': [{'scriptId': 'custcolfyle_expense_url', + 'type': 'String', 'value': 'None/app/admin/#/enterprise/view_expense/txcKVVELn1Vl?org_id=orHe8CpW2hyN' },{'scriptId': 'custcolfyle_expense_url_2', + 'type': 'String', 'value': 'None/app/admin/#/enterprise/view_expense/txcKVVELn1Vl?org_id=orHe8CpW2hyN' }], 'isBillable': False, 'taxAmount': None, 'taxCode': { - 'name': None, - 'internalId': None, 'externalId': None, - 'type': 'taxGroup', + 'internalId': None, + 'name': None, + 'type': 'taxGroup' }, }], 'externalId': 'cc-charge 48 - admin1@fyleforintacct.in', diff --git a/tests/test_netsuite/test_connector.py b/tests/test_netsuite/test_connector.py index 74726334..47d4217b 100644 --- a/tests/test_netsuite/test_connector.py +++ b/tests/test_netsuite/test_connector.py @@ -6,6 +6,7 @@ from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping, CategoryMapping from apps.netsuite.connector import NetSuiteConnector, NetSuiteCredentials from apps.workspaces.models import Configuration, Workspace +from apps.mappings.models import GeneralMapping from netsuitesdk import NetSuiteRequestError from tests.helper import dict_compare_keys from .fixtures import data @@ -18,22 +19,70 @@ def test_construct_expense_report(create_expense_report): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) expense_report, expense_report_lineitem = create_expense_report - expense_report = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem) + expense_report = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) data['expense_report_payload'][0]['tranDate'] = expense_report['tranDate'] data['expense_report_payload'][0]['expenseList'][0]['expenseDate'] = expense_report['expenseList'][0]['expenseDate'] assert expense_report == data['expense_report_payload'][0] +def test_construct_expense_report_with_tax_balancing(create_expense_report, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) + + # without tax balancing + expense_report, expense_report_lineitem = create_expense_report + expense_report_lineitem[0].amount = 100 + expense_report_lineitem[0].tax_amount = 3 + expense_report_lineitem[0].tax_item_id = '103578' + + expense_report_object = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) + + assert len(expense_report_object['expenseList']) == 1 + assert expense_report_object['expenseList'][0]['amount'] == 97 + assert expense_report_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert expense_report_object['expenseList'][0]['tax1Amt'] == 3 + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + expense_report_object = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) + + assert len(expense_report_object['expenseList']) == 2 + assert expense_report_object['expenseList'][0]['amount'] == 60 + assert expense_report_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert expense_report_object['expenseList'][0]['tax1Amt'] == 3 + assert expense_report_object['expenseList'][1]['amount'] == 37 + assert expense_report_object['expenseList'][1]['taxCode']['internalId'] == general_mapping.default_tax_code_id + + + # with tax balancing enabled and right tax amount + expense_report_lineitem[0].amount = 100 + expense_report_lineitem[0].tax_amount = 4.76 + expense_report_lineitem[0].tax_item_id = '103578' + + expense_report_object = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) + + assert len(expense_report_object['expenseList']) == 1 + assert expense_report_object['expenseList'][0]['amount'] == 95.24 + assert expense_report_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert expense_report_object['expenseList'][0]['tax1Amt'] == 4.76 + + general_mapping.is_tax_balancing_enabled = False + general_mapping.save() def test_construct_bill_account_based(create_bill_account_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) bill, bill_lineitem = create_bill_account_based - bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem) + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) data['bill_payload_account_based'][0]['tranDate'] = bill_object['tranDate'] data['bill_payload_account_based'][0]['tranId'] = bill_object['tranId'] @@ -44,9 +93,10 @@ def test_construct_bill_account_based(create_bill_account_based): def test_construct_bill_item_based(create_bill_item_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) bill, bill_lineitem = create_bill_item_based - bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem) + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) assert data['bill_payload_item_based']['expenseList'] == None assert dict_compare_keys(bill_object, data['bill_payload_item_based']) == [], 'construct bill_payload entry api return diffs in keys' @@ -55,20 +105,64 @@ def test_construct_bill_item_based(create_bill_item_based): def test_construct_bill_item_and_account_based(create_bill_item_and_account_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) bill, bill_lineitem = create_bill_item_and_account_based - bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem) + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) assert dict_compare_keys(bill_object, data['bill_payload_item_and_account_based']) == [], 'construct bill_payload entry api return diffs in keys' +def test_construct_bill_item_for_tax_balancing(create_bill_account_based, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) + + # without tax balancing + bill, bill_lineitem = create_bill_account_based + bill_lineitem[0].amount = 100 + bill_lineitem[0].tax_amount = 3 + bill_lineitem[0].tax_item_id = '103578' + + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) + + assert len(bill_object['expenseList']) == 1 + assert bill_object['expenseList'][0]['amount'] == 97 + assert bill_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert dict_compare_keys(bill_object, data['bill_payload_account_based'][0]) == [], 'construct bill_payload entry api return diffs in keys' + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) + assert len(bill_object['expenseList']) == 2 + assert bill_object['expenseList'][0]['amount'] == 60 + assert bill_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert bill_object['expenseList'][1]['amount'] == 37 + assert bill_object['expenseList'][1]['taxCode']['internalId'] == general_mapping.default_tax_code_id + + # with tax balancing enabled and right tax amount + bill_lineitem[0].amount = 100 + bill_lineitem[0].tax_amount = 4.76 + bill_lineitem[0].tax_item_id = '103578' + + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) + assert len(bill_object['expenseList']) == 1 + assert bill_object['expenseList'][0]['amount'] == 95.24 + assert bill_object['expenseList'][0]['taxCode']['internalId'] == '103578' + + general_mapping.is_tax_balancing_enabled = False + general_mapping.save() + def test_construct_journal_entry(create_journal_entry): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) configuration = Configuration.objects.get(workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) journal_entry, journal_entry_lineitem = create_journal_entry - journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration) + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) journal_entry_object['tranDate'] = data['journal_entry_without_single_line'][0]['tranDate'] @@ -77,7 +171,7 @@ def test_construct_journal_entry(create_journal_entry): configuration.je_single_credit_line = True configuration.save() - journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration) + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) # With flag being different, the output should be different assert journal_entry_object != data['journal_entry_without_single_line'][0] @@ -137,13 +231,66 @@ def test_construct_single_itemized_credit_line(create_journal_entry): assert constructed_lines == expected_lines +def test_construct_journal_entry_with_tax_balancing(create_journal_entry, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + configuration = Configuration.objects.get(workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) + + # without tax balancing + journal_entry, journal_entry_lineitem = create_journal_entry + journal_entry_lineitem[0].amount = 100 + journal_entry_lineitem[0].tax_amount = 3 + journal_entry_lineitem[0].tax_item_id = '103578' + + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) + + assert len(journal_entry_object['lineList']) == 2 + assert journal_entry_object['lineList'][1]['debit'] == 97 + assert journal_entry_object['lineList'][1]['taxCode']['internalId'] == '103578' + assert journal_entry_object['lineList'][1]['grossAmt'] == 100 + assert journal_entry_object['lineList'][1]['tax1Amt'] == 3 + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) + + assert len(journal_entry_object['lineList']) == 3 + assert journal_entry_object['lineList'][1]['debit'] == 60 + assert journal_entry_object['lineList'][1]['taxCode']['internalId'] == '103578' + assert journal_entry_object['lineList'][2]['debit'] == 37 + assert journal_entry_object['lineList'][2]['taxCode']['internalId'] == general_mapping.default_tax_code_id + assert journal_entry_object['lineList'][1]['grossAmt'] == 63 + assert journal_entry_object['lineList'][2]['grossAmt'] == 37 + assert journal_entry_object['lineList'][1]['tax1Amt'] == 3 + + # with tax balancing enabled and right tax amount + journal_entry_lineitem[0].amount = 100 + journal_entry_lineitem[0].tax_amount = 4.76 + journal_entry_lineitem[0].tax_item_id = '103578' + + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) + + assert len(journal_entry_object['lineList']) == 2 + assert journal_entry_object['lineList'][1]['debit'] == 95.24 + assert journal_entry_object['lineList'][1]['taxCode']['internalId'] == '103578' + assert journal_entry_object['lineList'][1]['tax1Amt'] == 4.76 + assert journal_entry_object['lineList'][1]['grossAmt'] == 100 + + general_mapping.is_tax_balancing_enabled = False + general_mapping.save() + + def test_contruct_credit_card_charge(create_credit_card_charge): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=49) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=49) + general_mapping = GeneralMapping.objects.get(workspace_id=49) credit_card_charge, credit_card_charge_lineitem = create_credit_card_charge - credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, []) + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) credit_card_charge_object['tranDate'] = data['credit_card_charge'][0]['tranDate'] credit_card_charge_object['tranid'] = data['credit_card_charge'][0]['tranid'] @@ -151,6 +298,47 @@ def test_contruct_credit_card_charge(create_credit_card_charge): assert credit_card_charge_object == data['credit_card_charge'][0] +def test_contruct_credit_card_charge_with_tax_balancing(create_credit_card_charge, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=49) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=49) + general_mapping = GeneralMapping.objects.get(workspace_id=49) + + # without tax balancing + credit_card_charge, credit_card_charge_lineitem = create_credit_card_charge + credit_card_charge_lineitem.amount = 100 + credit_card_charge_lineitem.tax_amount = 3 + credit_card_charge_lineitem.tax_item_id = '103578' + + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) + + assert len(credit_card_charge_object['expenses']) == 1 + assert credit_card_charge_object['expenses'][0]['amount'] == 97 + assert credit_card_charge_object['expenses'][0]['taxCode']['internalId'] == '103578' + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) + + assert len(credit_card_charge_object['expenses']) == 2 + assert credit_card_charge_object['expenses'][0]['amount'] == 60 + assert credit_card_charge_object['expenses'][0]['taxCode']['internalId'] == '103578' + assert credit_card_charge_object['expenses'][1]['amount'] == 37 + assert credit_card_charge_object['expenses'][1]['taxCode']['internalId'] == general_mapping.default_tax_code_id + + # with tax balancing enabled and right tax amount + credit_card_charge_lineitem.amount = 100 + credit_card_charge_lineitem.tax_amount = 4.76 + credit_card_charge_lineitem.tax_item_id = '103578' + + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) + + assert len(credit_card_charge_object['expenses']) == 1 + assert credit_card_charge_object['expenses'][0]['amount'] == 95.24 + assert credit_card_charge_object['expenses'][0]['taxCode']['internalId'] == '103578' + + def test_post_vendor(mocker, db): mocker.patch( 'netsuitesdk.api.vendors.Vendors.post', @@ -662,6 +850,7 @@ def test_post_bill_exception(db, mocker, create_bill_account_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=workspace_id) bill_transaction, bill_transaction_lineitems = create_bill_account_based @@ -671,7 +860,7 @@ def test_post_bill_exception(db, mocker, create_bill_account_based): with mock.patch('netsuitesdk.api.vendor_bills.VendorBills.post') as mock_call: mock_call.side_effect = [NetSuiteRequestError('An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.'), None] - netsuite_connection.post_bill(bill_transaction, bill_transaction_lineitems) + netsuite_connection.post_bill(bill_transaction, bill_transaction_lineitems, general_mapping) def test_post_expense_report_exception(db, mocker, create_expense_report): @@ -679,6 +868,7 @@ def test_post_expense_report_exception(db, mocker, create_expense_report): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=workspace_id) expense_report_transaction, expense_report_transaction_lineitems = create_expense_report @@ -688,7 +878,7 @@ def test_post_expense_report_exception(db, mocker, create_expense_report): with mock.patch('netsuitesdk.api.expense_reports.ExpenseReports.post') as mock_call: mock_call.side_effect = [NetSuiteRequestError('An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.'), None] - netsuite_connection.post_expense_report(expense_report_transaction, expense_report_transaction_lineitems) + netsuite_connection.post_expense_report(expense_report_transaction, expense_report_transaction_lineitems, general_mapping) def test_post_journal_entry_exception(db, mocker, create_journal_entry): @@ -696,6 +886,7 @@ def test_post_journal_entry_exception(db, mocker, create_journal_entry): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=workspace_id) journal_entry_transaction, journal_entry_transaction_lineitems = create_journal_entry @@ -707,7 +898,7 @@ def test_post_journal_entry_exception(db, mocker, create_journal_entry): with mock.patch('netsuitesdk.api.journal_entries.JournalEntries.post') as mock_call: mock_call.side_effect = [NetSuiteRequestError('An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.'), None] - netsuite_connection.post_journal_entry(journal_entry_transaction, journal_entry_transaction_lineitems, configuration) + netsuite_connection.post_journal_entry(journal_entry_transaction, journal_entry_transaction_lineitems, configuration, general_mapping) def test_update_destination_attributes(db, mocker): mocker.patch( @@ -828,4 +1019,52 @@ def test_skip_sync_attributes(mocker, db): new_project_count = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='CUSTOMER').count() assert new_project_count == 0 - \ No newline at end of file + +def test_constructs_tax_details_list_for_multiple_items(mocker, db): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + + # Create a more complete mock Mapping object + mock_mapping = mocker.Mock() + mock_mapping.destination.destination_id = 'tax_code_1' + mock_mapping.destination.detail.get.return_value = 'tax_type_1' + mock_mapping.destination.detail.all.return_value = [mocker.Mock(value=10.0)] + + # Mock get_tax_group_mapping to return our complete mock mapping + mocker.patch( + 'apps.netsuite.models.get_tax_group_mapping', + return_value=mock_mapping + ) + + # Creating mock expense objects with workspace_id and tax_group_id + expense1 = mocker.Mock( + amount=100.0, + tax_amount=10.0, + expense_number='EXP001', + workspace_id=1, + tax_group_id=1 + ) + + expense2 = mocker.Mock( + amount=200.0, + tax_amount=20.0, + expense_number='EXP002', + workspace_id=1, + tax_group_id=1 + ) + + # Creating mock bill line items with expense attribute and workspace_id + bill_lineitem1 = mocker.Mock( + expense=expense1, + workspace_id=1 + ) + bill_lineitem2 = mocker.Mock( + expense=expense2, + workspace_id=1 + ) + + bill_lineitems = [bill_lineitem1, bill_lineitem2] + + result = netsuite_connection.construct_tax_details_list(bill_lineitems) + + assert result == data['tax_list_detail'] diff --git a/tests/test_netsuite/test_tasks.py b/tests/test_netsuite/test_tasks.py index b5183f89..e3f7ab4f 100644 --- a/tests/test_netsuite/test_tasks.py +++ b/tests/test_netsuite/test_tasks.py @@ -175,7 +175,7 @@ def test_get_or_create_credit_card_vendor_create_false(mocker, db): assert created_vendor == None @pytest.mark.django_db() -def test_post_bill_success(mocker, db): +def test_post_bill_success(add_tax_destination_attributes, mocker, db): mocker.patch( 'netsuitesdk.api.vendor_bills.VendorBills.post', return_value=data['creation_response'] @@ -207,6 +207,9 @@ def test_post_bill_success(mocker, db): created_at='2023-07-07 11:57:53.184441+00', updated_at='2023-07-07 11:57:53.184441+00') expense_group = ExpenseGroup.objects.filter(workspace_id=workspace_id, fund_source='PERSONAL').first() + for expenses in expense_group.expenses.all(): + expenses.workspace_id = 2 + expenses.save() create_bill(expense_group, task_log.id, True) task_log = TaskLog.objects.get(pk=task_log.id)