diff --git a/dynamics/apis/api_base.py b/dynamics/apis/api_base.py index 0d3d5cb..ab3f92c 100644 --- a/dynamics/apis/api_base.py +++ b/dynamics/apis/api_base.py @@ -17,6 +17,8 @@ class ApiBase: def __init__(self): self.__access_token = None self.__server_url = None + self.__batch_url = None + self.company_id = None def change_access_token(self, access_token): """Change the old access token with the new one. @@ -34,12 +36,27 @@ def set_server_url(self, server_url): """ self.__server_url = server_url + def set_batch_url(self, batch_url): + """Set the batch URL dynamically upon creating a connection + + Parameters: + batch_url(str): The current batch URL + """ + self.__batch_url = batch_url + + def set_company_id(self, company_id): + """Set the company id dynamically upon creating a connection + + Parameters: + company_id(str): The current company id + """ + self.company_id = company_id + def _get_request(self, params, api_url): """Create a HTTP GET request. Parameters: params (dict): HTTP GET parameters for the wanted API. - api_url (str): Url for the wanted API. Returns: A response from the request (dict). @@ -182,4 +199,103 @@ def _patch_request(self, content_type, data, api_url): raise InternalServerError('Internal server error', response.text) else: - raise DynamicsError('Error: {0}'.format(response.status_code), response.text) \ No newline at end of file + raise DynamicsError('Error: {0}'.format(response.status_code), response.text) + + def _delete_request(self, params, api_url): + """ + + """ + api_headers = { + 'Authorization': self.__access_token, + 'Accept': 'application/json' + } + api_params = {} + + for k in params: + # ignore all unused params + if not params[k] is None: + p = params[k] + + # convert boolean to lowercase string + if isinstance(p, bool): + p = str(p).lower() + + api_params[k] = p + + response = requests.delete( + '{0}{1}'.format(self.__server_url, api_url), + headers=api_headers, + params=api_params + ) + + if response.status_code == 204: + return {'status': 'success'} + + elif response.status_code == 400: + raise WrongParamsError('Some of the parameters are wrong', response.text) + + elif response.status_code == 401: + raise InvalidTokenError('Invalid token, try to refresh it', response.text) + + elif response.status_code == 403: + raise NoPrivilegeError('Forbidden, the user has insufficient privilege', response.text) + + elif response.status_code == 404: + raise NotFoundItemError('Not found item with ID', response.text) + + elif response.status_code == 498: + raise ExpiredTokenError('Expired token, try to refresh it', response.text) + + elif response.status_code == 500: + raise InternalServerError('Internal server error', response.text) + + else: + raise DynamicsError('Error: {0}'.format(response.status_code), response.text) + + def _bulk_post_request(self, data, isolation: str): + """Create a HTTP batch request. + + Parameters: + data (dict): HTTP POST body data for the wanted API. + isolation: The isolation level for the batch request. + + Returns: + A response from the request (dict). + """ + + api_headers = { + 'Authorization': self.__access_token, + 'Accept': 'application/json', + 'Isolation': isolation + } + + response = requests.post( + '{0}'.format(self.__batch_url), + headers=api_headers, + json=data + ) + + if response.status_code == 200 or response.status_code == 201: + result = json.loads(response.text) + return result + + elif response.status_code == 400: + raise WrongParamsError('Some of the parameters are wrong', response.text) + + elif response.status_code == 401: + raise InvalidTokenError('Invalid token, try to refresh it', response.text) + + elif response.status_code == 403: + raise NoPrivilegeError('Forbidden, the user has insufficient privilege', response.text) + + elif response.status_code == 404: + raise NotFoundItemError('Not found item with ID', response.text) + + elif response.status_code == 498: + raise ExpiredTokenError('Expired token, try to refresh it', response.text) + + elif response.status_code == 500: + raise InternalServerError('Internal server error', response.text) + + else: + raise DynamicsError('Error: {0}'.format(response.status_code), response.text) diff --git a/dynamics/apis/invoice_line_items.py b/dynamics/apis/invoice_line_items.py index 5fe655a..4ae6753 100644 --- a/dynamics/apis/invoice_line_items.py +++ b/dynamics/apis/invoice_line_items.py @@ -1,4 +1,5 @@ from .api_base import ApiBase +from .invoices import PurchaseInvoices class PurchaseInvoiceLineItems(ApiBase): @@ -6,6 +7,9 @@ class PurchaseInvoiceLineItems(ApiBase): GET_PURCHASE_INVOICE_LINEITEMS = '/purchaseInvoices({0})/purchaseInvoiceLines' POST_PURCHASE_INVOICE_LINEITEM = '/purchaseInvoices({0})/purchaseInvoiceLines' + BULK_POST_PURCHASE_INVOICE_LINEITEM = 'purchaseInvoices({0})/purchaseInvoiceLines' + DELETE_PURCHASE_INVOICE_LINEITEM = '/purchaseInvoiceLines({0})' + def get_all(self, purchase_invoice_id: str, **kwargs): """ @@ -26,3 +30,43 @@ def post(self, purchase_invoice_id: str, data): return self._post_request( data, PurchaseInvoiceLineItems.POST_PURCHASE_INVOICE_LINEITEM.format(purchase_invoice_id) ) + + def delete(self, purchase_invoice_lineitem_id: str, **kwargs): + """ + Delete PurchaseInvoiceLineItem + :param purchase_invoice_lineitem_id: + :param kwargs: + :return: + """ + return self._delete_request({**kwargs}, PurchaseInvoiceLineItems.DELETE_PURCHASE_INVOICE_LINEITEM.format(purchase_invoice_lineitem_id)) + + def bulk_post(self, purchase_invoice_id: str, line_items: list, isolation: str = 'snapshot'): + """ + Create PurchaseInvoice LineItems in bulk. + + :param purchase_invoice_id: The ID of the purchase invoice. + :param line_items: A list of line items to be added to the purchase invoice. + :param isolation: The isolation level of the bulk post request. + :return: Bulk response containing the results of the bulk post operation. + """ + # Prepare payload for bulk post + bulk_payload = [] + for line_item in line_items: + # Prepare payload for each line item + line_item_payload = { + "method": "POST", + "url": PurchaseInvoiceLineItems.BULK_POST_PURCHASE_INVOICE_LINEITEM.format(purchase_invoice_id), + "headers": { + "CompanyId": self.company_id, + "Content-Type": "application/json", + "If-Match": "*" + }, + "body": line_item + } + bulk_payload.append(line_item_payload) + + # Create a bulk payload containing all line item requests + bulk_request_payload = {'requests': bulk_payload} + + # Make the bulk post request + return self._bulk_post_request(bulk_request_payload, isolation) diff --git a/dynamics/apis/invoices.py b/dynamics/apis/invoices.py index a106989..892e1ec 100644 --- a/dynamics/apis/invoices.py +++ b/dynamics/apis/invoices.py @@ -8,6 +8,7 @@ class PurchaseInvoices(ApiBase): GET_PURCHASE_INVOICES = '/purchaseInvoices' POST_PURCHASE_INVOICE = '/purchaseInvoices' + DELETE_PURCHASE_INVOICE = '/purchaseInvoices({0})' def get_all(self, **kwargs): """ @@ -25,3 +26,12 @@ def post(self, data): :return: """ return self._post_request(data, PurchaseInvoices.POST_PURCHASE_INVOICE) + + def delete(self, purchase_invoice_id: str, **kwargs): + """ + Delete PurchaseInvoice + :param purchase_invoice_id: + :param kwargs: + :return: + """ + return self._delete_request({**kwargs}, PurchaseInvoices.DELETE_PURCHASE_INVOICE.format(purchase_invoice_id)) diff --git a/dynamics/apis/journal.py b/dynamics/apis/journal.py index bf2be5d..f75ff75 100644 --- a/dynamics/apis/journal.py +++ b/dynamics/apis/journal.py @@ -8,6 +8,7 @@ class Journals(ApiBase): GET_JOURNALS = '/journals' POST_JOURNALS = '/journals' + DELETE_JOURNALS = '/journals({0})' def get_all(self, **kwargs): """ @@ -25,3 +26,12 @@ def post(self, data): :return: """ return self._post_request(data, Journals.POST_JOURNALS) + + def delete(self, journal_id: str, **kwargs): + """ + Delete Journals + :param journal_id: + :param kwargs: + :return: + """ + return self._delete_request({**kwargs}, Journals.DELETE_JOURNALS.format(journal_id)) diff --git a/dynamics/apis/journal_line_items.py b/dynamics/apis/journal_line_items.py index 9ff9668..abb9756 100644 --- a/dynamics/apis/journal_line_items.py +++ b/dynamics/apis/journal_line_items.py @@ -4,10 +4,12 @@ class JournalLineItem(ApiBase): - """Class for PurchaseInvoice APIs.""" + """Class for Journal LineItem APIs.""" GET_JOURNAL_LINE_ITEMS = '/journals({0})/journalLines' POST_JOURNAL_LINE_ITEMS = '/journals({0})/journalLines' + BULK_POST_JOURNAL_LINEITEM = 'journals({0})/journalLines' + DELETE_JOURNAL_LINE_ITEMS = '/journalLines({0})' def get_all(self, jounal_id, **kwargs): """ @@ -25,3 +27,43 @@ def post(self, journal_id, data): :return: """ return self._post_request(data, JournalLineItem.POST_JOURNAL_LINE_ITEMS.format(journal_id)) + + def delete(self, jounral_lineitem_id: str, **kwargs): + """ + Delete Journal Line Item + :param jounral_lineitem_id: + :param kwargs: + :return: + """ + return self._delete_request({**kwargs}, JournalLineItem.DELETE_JOURNAL_LINE_ITEMS.format(jounral_lineitem_id)) + + def bulk_post(self, journal_id: str, line_items: list, isolation: str = 'snapshot'): + """ + Create Journal LineItems in bulk. + + :param journal_id: The ID of the journal. + :param line_items: A list of line items to be added to the journal line items. + :param isolation: The isolation level of the bulk post request. + :return: Bulk response containing the results of the bulk post operation. + """ + # Prepare payload for bulk post + bulk_payload = [] + for line_item in line_items: + # Prepare payload for each line item + line_item_payload = { + "method": "POST", + "url": JournalLineItem.BULK_POST_JOURNAL_LINEITEM.format(journal_id), + "headers": { + "CompanyId": self.company_id, + "Content-Type": "application/json", + "If-Match": "*" + }, + "body": line_item + } + bulk_payload.append(line_item_payload) + + # Create a bulk payload containing all line item requests + bulk_request_payload = {'requests': bulk_payload} + + # Make the bulk post request + return self._bulk_post_request(bulk_request_payload, isolation) diff --git a/dynamics/core/client.py b/dynamics/core/client.py index 56c84d1..e9b11a2 100644 --- a/dynamics/core/client.py +++ b/dynamics/core/client.py @@ -17,7 +17,7 @@ def __init__( client_secret: str, environment: str, refresh_token: str, - company_id: str = None + company_id: str = None ): """ Constructor to initialize the Dynamics SDK. @@ -48,6 +48,8 @@ def __init__( # Get and set the access token access_token = self.__refresh_access_token() self.set_server_url() + self.set_batch_url() + self.set_company_id() self.update_access_token(access_token) def update_access_token(self, access_token: str): @@ -74,6 +76,44 @@ def update_access_token(self, access_token: str): for api in api_instances: api.change_access_token(token) + def set_company_id(self): + """ + Set the Company ID in all API objects. + """ + api_instances = [ + self.purchase_invoice_line_items, + self.journal_line_items + ] + + # Set company ID for all API instances + for api in api_instances: + api.set_company_id(self.__company_id) + + def set_batch_url(self): + """ + Set the Batch URL in all API objects. + """ + batch_url = self.BASE_URL.format(environment=self.__environment) + + batch_url = '{0}{1}'.format(batch_url, '/$batch') + + api_instances = [ + self.companies, + self.vendors, + self.accounts, + self.purchase_invoices, + self.journals, + self.journal_line_items, + self.purchase_invoice_line_items, + self.attachments, + self.employees, + self.locations + ] + + # Set batch URL for all API instances + for api in api_instances: + api.set_batch_url(batch_url) + def set_server_url(self): """ Set the Base URL in all API objects. diff --git a/setup.py b/setup.py index 55254a3..fb7e5c9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='ms-dynamics-business-central-sdk', - version='1.2.0', + version='1.3.0', author='Shwetabh Kumar', author_email='shwetabh.kumar@fyle.in', description='Python SDK for accessing Dynamics APIs',