diff --git a/.DS_Store b/.DS_Store index ded2b90..80f31a2 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/tap_quickbooks/__init__.py b/tap_quickbooks/__init__.py index 204fbae..e4756ec 100644 --- a/tap_quickbooks/__init__.py +++ b/tap_quickbooks/__init__.py @@ -285,7 +285,9 @@ def main_impl(): gl_full_sync = CONFIG.get('gl_full_sync'), gl_weekly = CONFIG.get('gl_weekly', False), gl_daily = CONFIG.get('gl_daily', False), - gl_basic_fields = CONFIG.get('gl_basic_fields', False)) + gl_basic_fields = CONFIG.get('gl_basic_fields', False), + hg_sync_output = CONFIG.get('hg_sync_output') + ) qb.login() if args.discover: diff --git a/tap_quickbooks/quickbooks/__init__.py b/tap_quickbooks/quickbooks/__init__.py index f54507d..3cec2dc 100644 --- a/tap_quickbooks/quickbooks/__init__.py +++ b/tap_quickbooks/quickbooks/__init__.py @@ -290,6 +290,7 @@ def __init__(self, gl_weekly = None, gl_daily = None, gl_basic_fields = None, + hg_sync_output = None, realm_id=None): self.api_type = api_type.upper() if api_type else None self.report_period_days = report_period_days @@ -306,6 +307,7 @@ def __init__(self, self.qb_client_secret = qb_client_secret self.session = requests.Session() self.access_token = None + self.hg_sync_output = hg_sync_output self.base_url = "https://sandbox-quickbooks.api.intuit.com/v3/company/" if is_sandbox is True else 'https://quickbooks.api.intuit.com/v3/company/' diff --git a/tap_quickbooks/quickbooks/rest.py b/tap_quickbooks/quickbooks/rest.py index 4d50545..bb832db 100644 --- a/tap_quickbooks/quickbooks/rest.py +++ b/tap_quickbooks/quickbooks/rest.py @@ -107,7 +107,7 @@ def _sync_records(self, url, headers, params, stream): headers.update(self.qb._get_standard_headers()) records_deleted = [] excluded_entities = ["Bill", "Payment", "Transfer", "CompanyInfo", "CreditMemo", "Invoice", - "JournalEntry", "Preferences", "Purchase", "SalesReceipt", "TimeActivity", "BillPayment","Estimate"] + "JournalEntry", "Preferences", "Purchase", "SalesReceipt", "TimeActivity", "BillPayment","Estimate","Attachable"] if self.qb.include_deleted and stream not in excluded_entities: # Get the deleted records first if "WHERE" in query: diff --git a/tap_quickbooks/quickbooks/schemas/object_definition.json b/tap_quickbooks/quickbooks/schemas/object_definition.json index 28de90c..afde323 100644 --- a/tap_quickbooks/quickbooks/schemas/object_definition.json +++ b/tap_quickbooks/quickbooks/schemas/object_definition.json @@ -666,5 +666,19 @@ {"name": "TaxExemptionRef", "type": "string"}, {"name": "HomeTotalAmt", "type": "string"}, {"name": "FreeFormAddress", "type": "boolean"} + ], + + "Attachable": [ + {"name": "FileName", "type": "string"}, + {"name": "FileAccessUri", "type": "string"}, + {"name": "TempDownloadUri", "type": "string"}, + {"name": "Size", "type": "number"}, + {"name": "ContentType", "type": "string"}, + {"name": "domain", "type": "string"}, + {"name": "sparse", "type": "boolean"}, + {"name": "Id", "type": "string"}, + {"name": "SyncToken", "type": "string"}, + {"name": "MetaData", "type": "string"}, + {"name": "AttachableRef", "type": "array","child_type": "string"} ] } diff --git a/tap_quickbooks/sync.py b/tap_quickbooks/sync.py index 80e270f..7d6f018 100644 --- a/tap_quickbooks/sync.py +++ b/tap_quickbooks/sync.py @@ -1,3 +1,4 @@ +import os import time import singer import singer.utils as singer_utils @@ -6,6 +7,7 @@ from jsonpath_ng import jsonpath, parse import math import json +import requests LOGGER = singer.get_logger() @@ -93,7 +95,19 @@ def sync_records(qb, catalog_entry, state, counter, state_passed): query_func = qb.query_report for rec in query_func(catalog_entry, state, state_passed): - + #Check if it is Attachable stream with a downloadable file + if stream == 'Attachable' and "TempDownloadUri" in rec: + file_name = rec["FileName"] + attachable_ref = rec.get("AttachableRef",[]) + if len(attachable_ref)>0 and "EntityRef" in attachable_ref[0]: + attachable_ref = attachable_ref[0]['EntityRef'] + if attachable_ref: + file_name = f"{attachable_ref['type']}-{attachable_ref['value']}-{file_name}" + #Save the newly formatted file name + rec['FileName'] = file_name + download_file( + rec["TempDownloadUri"], os.path.join(qb.hg_sync_output or "", file_name) + ) counter.increment() with Transformer(pre_hook=transform_data_hook) as transformer: rec = transformer.transform(rec, schema) @@ -136,3 +150,19 @@ def sync_records(qb, catalog_entry, state, counter, state_passed): singer.write_message(activate_version_message) state = singer.write_bookmark( state, catalog_entry['tap_stream_id'], 'version', None) + +def download_file(url, local_filename): + # Send an HTTP GET request to the URL + response = requests.get(url, stream=True) + LOGGER.info(f"Downloading file: {local_filename}") + # Check if the request was successful (status code 200) + if response.status_code == 200: + # Open a local file with write-binary mode to save the downloaded content + with open(local_filename, 'wb') as f: + # Iterate over the content of the response in chunks and write to the file + for chunk in response.iter_content(chunk_size=1024): + f.write(chunk) + LOGGER.info(f"File downloaded successfully: {local_filename}") + else: + LOGGER.info(f"Failed to download file. HTTP status code: {response.status_code}") +