diff --git a/netsuitesdk/api/employees.py b/netsuitesdk/api/employees.py new file mode 100644 index 0000000..217426c --- /dev/null +++ b/netsuitesdk/api/employees.py @@ -0,0 +1,9 @@ +from .base import ApiBase +import logging + +logger = logging.getLogger(__name__) + + +class Employees(ApiBase): + def __init__(self, ns_client): + ApiBase.__init__(self, ns_client=ns_client, type_name='Employee') diff --git a/netsuitesdk/api/expense_reports.py b/netsuitesdk/api/expense_reports.py new file mode 100644 index 0000000..098bb14 --- /dev/null +++ b/netsuitesdk/api/expense_reports.py @@ -0,0 +1,63 @@ +from collections import OrderedDict + +from .base import ApiBase +import logging + +from netsuitesdk.internal.utils import PaginatedSearch + +logger = logging.getLogger(__name__) + + +class ExpenseReports(ApiBase): + """ + ExpenseReports are not directly searchable - only via as employees + """ + + def __init__(self, ns_client): + ApiBase.__init__(self, ns_client=ns_client, type_name='ExpenseReport') + + def get_all_generator(self): + record_type_search_field = self.ns_client.SearchStringField(searchValue='ExpenseReport', operator='contains') + basic_search = self.ns_client.basic_search_factory('Employee', recordType=record_type_search_field) + paginated_search = PaginatedSearch(client=self.ns_client, + type_name='Employee', + basic_search=basic_search, + pageSize=20) + return self._paginated_search_to_generator(paginated_search=paginated_search) + + def post(self, data) -> OrderedDict: + assert data['externalId'], 'missing external id' + er = self.ns_client.ExpenseReport() + expense_list = [] + for eod in data['expenseList']: + ere = self.ns_client.ExpenseReportExpense(**eod) + expense_list.append(ere) + + er['expenseList'] = self.ns_client.ExpenseReportExpenseList(expense=expense_list) + er['expenseReportCurrency'] = self.ns_client.RecordRef(**(data['expenseReportCurrency'])) + + if 'memo' in data: + er['memo'] = data['memo'] + + if 'tranId' in data: + er['tranId'] = data['tranId'] + + if 'class' in data: + er['class'] = data['class'] + + if 'location' in data: + er['location'] = data['location'] + + if 'department' in data: + er['department'] = data['department'] + + if 'account' in data: + er['account'] = self.ns_client.RecordRef(**(data['account'])) + + if 'externalId' in data: + er['externalId'] = data['externalId'] + + er['entity'] = self.ns_client.RecordRef(**(data['entity'])) + logger.debug('able to create er = %s', er) + res = self.ns_client.upsert(er) + return self._serialize(res) diff --git a/netsuitesdk/connection.py b/netsuitesdk/connection.py index ebfff78..bd4bd85 100644 --- a/netsuitesdk/connection.py +++ b/netsuitesdk/connection.py @@ -7,6 +7,8 @@ from .api.vendors import Vendors from .api.subsidiaries import Subsidiaries from .api.journal_entries import JournalEntries +from .api.employees import Employees +from .api.expense_reports import ExpenseReports from .internal.client import NetSuiteClient @@ -28,3 +30,5 @@ def __init__(self, account, consumer_key, consumer_secret, token_key, token_secr self.vendors = Vendors(ns_client) self.subsidiaries = Subsidiaries(ns_client) self.journal_entries = JournalEntries(ns_client) + self.employees = Employees(ns_client) + self.expense_reports = ExpenseReports(ns_client) diff --git a/netsuitesdk/internal/netsuite_types.py b/netsuitesdk/internal/netsuite_types.py index e9c2f9c..9ede4ac 100644 --- a/netsuitesdk/internal/netsuite_types.py +++ b/netsuitesdk/internal/netsuite_types.py @@ -42,6 +42,7 @@ 'TransactionSearchBasic', 'VendorSearchBasic', 'SubsidiarySearchBasic', + 'EmployeeSearchBasic', ], # urn:relationships.lists.webservices.netsuite.com @@ -84,6 +85,19 @@ 'JournalEntryLine', 'JournalEntryLineList', ], + + # https://webservices.netsuite.com/xsd/lists/v2019_2_0/employees.xsd + 'ns34': [ + 'EmployeeSearch', + ], + + # urn:employees_2019_2.transactions.webservices.netsuite.com + # https://webservices.netsuite.com/xsd/transactions/v2019_2_0/employees.xsd + 'ns38': [ + 'ExpenseReport', + 'ExpenseReportExpense', + 'ExpenseReportExpenseList', + ], } SIMPLE_TYPES = { diff --git a/test/integration/data/expense_reports/tstdrv2089588.json b/test/integration/data/expense_reports/tstdrv2089588.json new file mode 100644 index 0000000..5c20fad --- /dev/null +++ b/test/integration/data/expense_reports/tstdrv2089588.json @@ -0,0 +1,97 @@ +{ + "nullFieldList": null, + "createdDate": null, + "lastModifiedDate": null, + "status": null, + "customForm": null, + "account": { + "name": null, + "internalId": "25", + "externalId": null, + "type": "account" + }, + "entity": { + "name": null, + "internalId": "1648", + "externalId": null, + "type": "vendor" + }, + "expenseReportCurrency": { + "name": "USD", + "internalId": null, + "externalId": null, + "type": "currency" + }, + "expenseReportExchangeRate": null, + "subsidiary": { + "name": null, + "internalId": "1", + "externalId": null, + "type": "subsidiary" + }, + "taxPointDate": null, + "tranId": null, + "acctCorpCardExp": null, + "postingPeriod": null, + "tranDate": null, + "dueDate": null, + "approvalStatus": null, + "total": null, + "nextApprover": null, + "advance": null, + "tax1Amt": null, + "amount": null, + "memo": "Testing ExpenseReport using Fyle SDK", + "complete": null, + "supervisorApproval": null, + "accountingApproval": null, + "useMultiCurrency": null, + "tax2Amt": null, + "department": null, + "class": null, + "location": null, + "expenseList": [ + { + "amount": 100, + "category": { + "name": null, + "internalId": "2", + "externalId": null, + "type": "account" + }, + "class": null, + "corporateCreditCard": null, + "currency": { + "name": "USD", + "internalId": "1", + "externalId": null, + "type": "currency" + }, + "customer": null, + "customFieldList": null, + "department": null, + "exchangeRate": null, + "expenseDate": null, + "expMediaItem": null, + "foreignAmount": null, + "grossAmt": null, + "isBillable": null, + "isNonReimbursable": null, + "line": null, + "location": null, + "memo": "Testing ExpenseReports using Fyle SDK", + "quantity": null, + "rate": null, + "receipt": null, + "refNumber": null, + "tax1Amt": null, + "taxCode": null, + "taxRate1": null, + "taxRate2":null + } + ], + "accountingBookDetailList": null, + "customFieldList": null, + "internalId": null, + "externalId": "EXPR_1" +} diff --git a/test/integration/test_expense_reports.py b/test/integration/test_expense_reports.py new file mode 100644 index 0000000..9cf5899 --- /dev/null +++ b/test/integration/test_expense_reports.py @@ -0,0 +1,36 @@ +import logging +import pytest +import json +import os + +logger = logging.getLogger(__name__) + +def test_get(nc): + data = next(nc.expense_reports.get_all_generator()) + logger.debug('data = %s', data) + assert data, 'get all generator didnt work' + assert data['externalId'] == 'entity-5', f'No object found with externalId' + assert data['internalId'] == '-5', f'No object found with internalId' + + data = nc.expense_reports.get(externalId='EXPR_1') + logger.debug('data = %s', data) + assert data, f'No object with externalId' + assert data['externalId'] == 'EXPR_1', f'No object with externalId' + assert data['internalId'] == '10613', f'No object with internalId' + +def test_post(nc): + filename = os.getenv('NS_ACCOUNT').lower() + '.json' + with open('./test/integration/data/expense_reports/' + filename) as oj: + s = oj.read() + expr1 = json.loads(s) + logger.debug('expr1 = %s', expr1) + res = nc.expense_reports.post(expr1) + logger.debug('res = %s', res) + assert res['externalId'] == expr1['externalId'], 'External ID does not match' + assert res['type'] == 'expenseReport', 'Type does not match' + + expr2 = nc.expense_reports.get(externalId=res['externalId']) + logger.debug('expr2 = %s', expr2) + assert expr2['amount'] == 100.0, 'Amount does not match' + assert expr2['externalId'] == 'EXPR_1', 'External ID does not match' + assert expr2['internalId'] == '10613', 'Internal ID does not match' diff --git a/test/internal/test_get.py b/test/internal/test_get.py index 23b20fe..c8e835c 100644 --- a/test/internal/test_get.py +++ b/test/internal/test_get.py @@ -17,6 +17,14 @@ def test_get_journal_entry(ns): record = ns.get(recordType='journalEntry', externalId='JE_01') assert record, 'No journal entry found' +def test_get_employee(ns): + record = ns.get(recordType='employee', internalId='1648') + assert record, 'No employee record for internalId 1' + +def test_get_expense_report(ns): + record = ns.get(recordType='ExpenseReport', externalId='EXPR_1') + assert record, 'No expense report found' + # def test_get_currency1(nc): # currency = nc.currency.get(internal_id='1') # logger.info('currency is %s', currency) \ No newline at end of file diff --git a/test/internal/test_get_all.py b/test/internal/test_get_all.py index 0c6e18a..90d40d4 100644 --- a/test/internal/test_get_all.py +++ b/test/internal/test_get_all.py @@ -9,7 +9,7 @@ def test_get_all(ns, type_name): records = ns.getAll(recordType=type_name) assert len(records) > 0, f'No records of type {type_name} returned' -@pytest.mark.parametrize('type_name', ['account', 'vendor', 'department', 'location', 'classification', 'subsidiaries']) +@pytest.mark.parametrize('type_name', ['account', 'vendor', 'department', 'location', 'classification', 'subsidiaries', 'employees']) def test_get_all_not_supported(ns, type_name): with pytest.raises(zeep.exceptions.Fault) as ex: records = ns.getAll(recordType=type_name) diff --git a/test/internal/test_search.py b/test/internal/test_search.py index 6c021c7..e388fe0 100644 --- a/test/internal/test_search.py +++ b/test/internal/test_search.py @@ -25,7 +25,17 @@ def test_search_journal_entries(ns): assert len(paginated_search.records) > 0, 'There are no journal entries' logger.debug('record = %s', str(paginated_search.records[0])) -@pytest.mark.parametrize('type_name', ['Account', 'Vendor', 'Department', 'Location', 'Classification']) +def test_search_expense_reports(ns): + record_type_search_field = ns.SearchStringField(searchValue='ExpenseReport', operator='contains') + basic_search = ns.basic_search_factory('Transaction', recordType=record_type_search_field) + paginated_search = PaginatedSearch(client=ns, + type_name='Transaction', + basic_search=basic_search, + pageSize=5) + assert len(paginated_search.records) > 0, 'There are no expense reports' + logger.debug('record = %s', str(paginated_search.records[0])) + +@pytest.mark.parametrize('type_name', ['Account', 'Vendor', 'Department', 'Location', 'Classification', 'Subsidiary', 'Employee']) def test_search_all(ns, type_name): paginated_search = PaginatedSearch(client=ns, type_name=type_name, pageSize=20) assert len(paginated_search.records) > 0, f'There are no records of type {type_name}' diff --git a/test/internal/test_upsert.py b/test/internal/test_upsert.py index de40c89..0ba4c7a 100644 --- a/test/internal/test_upsert.py +++ b/test/internal/test_upsert.py @@ -28,6 +28,9 @@ def get_category_account(ns): def get_currency(ns): return ns.get(recordType='currency', internalId='1') +def get_employee(ns): + return ns.get(recordType='employee', internalId='1648') + def test_upsert_vendor_bill(ns): vendor_ref = ns.RecordRef(type='vendor', internalId=get_vendor(ns).internalId) bill_account_ref = ns.RecordRef(type='account', internalId=25) @@ -113,3 +116,38 @@ def test_upsert_journal_entry(ns): je = ns.get(recordType='journalEntry', externalId='JE_1234') logger.debug('je = %s', str(je)) assert (je['externalId'] == 'JE_1234'), 'Journal Entry External ID does not match' + + +def test_upsert_expense_report(ns): + employee_ref = ns.RecordRef(type='employee', internalId=get_employee(ns).internalId) + bill_account_ref = ns.RecordRef(type='account', internalId=25) + cat_account_ref = ns.RecordRef(type='account', internalId='1') + loc_ref = ns.RecordRef(type='location', internalId=get_location(ns).internalId) + dep_ref = ns.RecordRef(type='department', internalId=get_department(ns).internalId) + class_ref = ns.RecordRef(type='classification', internalId=get_department(ns).internalId) + currency_ref = ns.RecordRef(type='currency', internalId=get_currency(ns).internalId) + expenses = [] + + er = ns.ExpenseReportExpense() + er['category'] = cat_account_ref + er['amount'] = 10.0 + er['department'] = dep_ref + er['class'] = class_ref + er['location'] = loc_ref + er['currency'] = currency_ref + + expenses.append(er) + + expense_report = ns.ExpenseReport(externalId='EXPR_1') + expense_report['expenseReportCurrency'] = currency_ref # US dollar + expense_report['exchangerate'] = 1.0 + expense_report['expenseList'] = ns.ExpenseReportExpenseList(expense=expenses) + expense_report['memo'] = 'test memo' + expense_report['entity'] = employee_ref + logger.debug('upserting expense report %s', expense_report) + record_ref = ns.upsert(expense_report) + logger.debug('record_ref = %s', record_ref) + assert record_ref['externalId'] == 'EXPR_1', 'External ID does not match' + + expr = ns.get(recordType='ExpenseReport', externalId='EXPR_1') + logger.debug('expense report = %s', str(expr))