[Feature] ERPNext Quickbooks Migrator (#15718)

* Create QuickBooks Connector Single DocType

* Create interface for user authorization and obtaining authorization code

* Obtain Access token using authorization code

* Fetch a random customer

* Schedule fetching as a background job

* Save fetched customer

* Fetch all customers, take care of pagination

* Save fetched customers

* Create a custom field for storing Quickbooks ID

* Don't save already saved customers

* Commit every successful insert

* No need of allow_guest=True

* Fetch Items as well

* Store Customer Addresses as well.

* Remove redundant custom field creation code

* Stupid refactoring

* Some more refactoring

* Fetch and Save Suppliers as well

* Save accounts. Really a hack as of now.

* Fetch and save invoices, Take care of child items as well

* Fetch Taxes as well

* Set currency on Sales Invoice

* Correctly Link Items, Handle markups

* Don't enqueue, Need to wait forever for testing sometimes

* Set margin rate correctly

* Correct fieldname is tax_amount not amount

* Fetch with maximum possible batch size

* Don't print unnecessary info

* Fetch Journal Entries

* Fetch Purchase Invoices (Bill)

* Don't perform reauthentication if you already have an access_token

* Don't need those print statements anymore

* Refresh access_token if request fails

* Fetch Payment Entry(Payment)

* Map QB items to ERPNext rather than other way around

* Don't need a huge comment block as of now

* Fetch Payment Entries against Purchase Invoices (BillPayment)

* Rename Quickbooks Connector to Migrator

* Make oauth settings configurable

* Make company configurable

* Make default accounts configurable

* Fetch accounts and other masters separately

* Show realtime progress

* Check if entries exist before attempting insert

* Suppress other annoying messages

* Tracebacks are good for health

* Don't want rounded total

* Don't call fetch after authorization (Need to somehow separate these)

* Make credit_to account payable

* When generating payment entry mention bank_account as well

* Cleanup

* Add custom buttons and reflect current state in form ui

* Show necessary fields on form depending on the current state

* Cleanup

* Minor fixes

* Set income and expense account on items.

* Set currency for Account.

* Stupid VSCode

* Remove redundant code

* Check for existing Payment and BillPayment correctly

* Cache API response for faster development

* Don't maintain stock for now, Seems to solve the issue with 'Stock Received but not billed'

* Cleanup

* Add methods to remove inserted data completely (Development)

* Don't commit in every iteration

* Set account head based on TaxRate in Sales Invoice

* Fetch and cache TaxCode

* Add methods to fetch TaxCode and TaxRate from cache

* Set item wise tax breakup, Don't use Actual Tax

* Use both TaxRateLists

* Set Itemwise tax on Purchase Invoice as well

* Set bank_amount, while creating payment entries

* Remove print statements

* Add Shipping in taxes child table of Sales Invoice

* Set posting date on Payment Entries against Invoices

* Fetch and save expenses as Journal Entries

* Fetch and save Deposits as Journal Entries

* Fetch and Save Credit and Debit Notes

* Fetch and save SalesReceipt

* Record Shipping as Item instead of Tax

* Minor Fixes

* Fetch Advance Payments

* Set account_type during Account creation

* Choose receivable account in Invoice based on currency

* Remove receivable_account field

* Account type is already set during account creation

* Set receivable account based on currency in Sales Receipt and Credit Memo

* Set receivable account on Customer

* Save Company Preferences

* Set shipping_account based on company preferences

* Set cost center in Sales Invoice Items and Taxes

* Make Account name unique in case of duplicate account name

* Fetch and Save TaxPayment as Journal Entry

* Record UndepositedFunds Account in cache if found

* Credit UndepositedFunds account when mentioned in Deposit

* Debit Cashback account as well if mentioned in Deposit

* Set currency conversion rate

* Set is_group field correctly for accounts

* Preserve account hierarchy

* Use quickbooks_id and company together for filtering

* Make new custom field 'company' on Supplier Customer and Item

* Use comapny field for filtering Customer, Item, Supplier

* Shipping Account is only available if shipping is enabled for comapny

* Invoice might not have TxnTaxDetail.TaxLine

* Lookup TaxRate account correctly

* TaxCode might not have SalesTaxRateList PurchaseTaxRateList or Both

* TaxCode might not have SalesTaxRateList and PurchaseTaxRateList

* Remove unnecessary UI elements

* Allow multiple companies to have customers with same name

* Refactor account_type

* Create a leaf an account for every group account

* Item and Tax list creation needs more information

* Supports discount in Invoice

* Save Invoice as Journal Entry when itemwise details are unavailable

* Credit=True represents a refund rather than charge

* Mode of Payment not synced, setting as Cash for now

* Tax code may also be set for an entire invoice

* Fetch General Ledger Report correctly

* Refactor fetching entries from General Ledger Report

* Tax Payment is the same as Sales Tax Payment and Purchase Tax Payment

* append_number_if_name_exists is stupid, Reimplementing

* Don't guess credit/debit for Tax Payment and Advanced Payment

* Bill Payment shouldn't assume full payment

* Save Payment as Journal Entry

* Make Client ID and Client Secret Default

* Remove unused python imports

* removed are_accounts_synced()

* Don't store translated values in database

* Endpoints as a field

* Major refactor - functions to methods

* Major Refactor Functions to Methods

* Major Refactor Fix Stuff

* Minor Changes

* Save Refund As well

* Minor Refactor (SI varations to SI, PI variations to PI)

* Record Inventory Qty Adjust Entries

* QBM Final Touches

* Make changes suggested in review
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/__init__.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js
new file mode 100644
index 0000000..bfffcc5
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.js
@@ -0,0 +1,71 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('QuickBooks Migrator', {
+	connect: function(frm) {
+		// OAuth requires user intervention to provide application access permissionsto requested scope
+		// Here we open a new window and redirect user to the authorization url.
+		// After user grants us permission to access. We will set authorization details on this doc which will force refresh.
+		window.open(frm.doc.authorization_url)
+	},
+	fetch_data: function(frm) {
+		frm.call("migrate")
+	},
+	onload: function(frm) {
+		frm.trigger("set_indicator")
+		var domain = frappe.urllib.get_base_url()
+		var redirect_url = `${domain}/api/method/erpnext.erpnext_integrations.doctype.quickbooks_migrator.quickbooks_migrator.callback`
+		if (frm.doc.redirect_url != redirect_url) {
+			frm.set_value("redirect_url", redirect_url)
+		}
+		// Instead of changing percentage width and message of single progress bar
+		// Show a different porgress bar for every action after some time remove the finished progress bar
+ 		// Former approach causes the progress bar to dance back and forth.
+		frm.trigger("set_indicator")
+ 		frappe.realtime.on("quickbooks_progress_update", function (data) {
+			frm.dashboard.show_progress(data.message, (data.count / data.total) * 100, data.message)
+			if (data.count == data.total) {
+				window.setTimeout( function(message) {frm.dashboard.hide_progress(message)}, 1500, data.messsage)
+			}
+		})
+	},
+	refresh: function(frm) {
+		frm.trigger("set_indicator")
+		if (!frm.doc.access_token) {
+			// Unset access_token signifies that we don't have enough information to connect to quickbooks api and fetch data
+			if (frm.doc.authorization_url) {
+				frm.add_custom_button(__("Connect to Quickbooks"), function () {
+					frm.trigger("connect")
+				});
+			}
+		}
+		if (frm.doc.access_token) {
+			// If we have access_token that means we also have refresh_token we don't need user intervention anymore
+			// All we need now is a Company from erpnext
+			frm.remove_custom_button(__("Connect to Quickbooks"))
+
+			frm.toggle_display("company_settings", 1)
+			frm.set_df_property("company", "reqd", 1)
+			if (frm.doc.company) {
+					frm.add_custom_button(__("Fetch Data"), function () {
+					frm.trigger("fetch_data")
+				});
+			}
+		}
+	},
+	set_indicator: function(frm) {
+		var indicator_map = {
+			"Connecting to QuickBooks": [__("Connecting to QuickBooks"), "orange"],
+			"Connected to QuickBooks": [__("Connected to QuickBooks"), "green"],
+			"In Progress": [__("In Progress"), "orange"],
+			"Complete": [__("Complete"), "green"],
+			"Failed": [__("Failed"), "red"],
+		}
+		if (frm.doc.status) {
+			var indicator = indicator_map[frm.doc.status]
+			var label = indicator[0]
+			var color = indicator[1]
+			frm.page.set_indicator(label, color)
+		}
+	},
+});
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json
new file mode 100644
index 0000000..b156006
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.json
@@ -0,0 +1,843 @@
+{
+ "allow_copy": 0,
+ "allow_events_in_timeline": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 1,
+ "creation": "2018-07-10 14:48:16.757030",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Status",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 1,
+   "collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url",
+   "columns": 0,
+   "fieldname": "application_settings",
+   "fieldtype": "Section Break",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Application Settings",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "",
+   "fieldname": "client_id",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 1,
+   "in_standard_filter": 0,
+   "label": "Client ID",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "",
+   "fieldname": "redirect_url",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 1,
+   "in_standard_filter": 0,
+   "label": "Redirect URL",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
+   "fieldname": "token_endpoint",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Token Endpoint",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 1,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "application_column_break",
+   "fieldtype": "Column Break",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "",
+   "fieldname": "client_secret",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 1,
+   "in_standard_filter": 0,
+   "label": "Client Secret",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "com.intuit.quickbooks.accounting",
+   "fieldname": "scope",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 1,
+   "in_standard_filter": 0,
+   "label": "Scope",
+   "length": 0,
+   "no_copy": 0,
+   "options": "",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 1,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "https://quickbooks.api.intuit.com/v3",
+   "fieldname": "api_endpoint",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "API Endpoint",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 1,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 1,
+   "columns": 0,
+   "fieldname": "authorization_settings",
+   "fieldtype": "Section Break",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Authorization Settings",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "default": "https://appcenter.intuit.com/connect/oauth2",
+   "fieldname": "authorization_endpoint",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Authorization Endpoint",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 1,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "refresh_token",
+   "fieldtype": "Small Text",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Refresh Token",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "code",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Code",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "authorization_column_break",
+   "fieldtype": "Column Break",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "authorization_url",
+   "fieldtype": "Data",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Authorization URL",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 1,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 1,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "access_token",
+   "fieldtype": "Small Text",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Access Token",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "quickbooks_company_id",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Quickbooks Company ID",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "company_settings",
+   "fieldtype": "Section Break",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Company Settings",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Company",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Company",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "default_shipping_account",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Default Shipping Account",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Account",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "default_warehouse",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Default Warehouse",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Warehouse",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "company_column_break",
+   "fieldtype": "Column Break",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "default_cost_center",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Default Cost Center",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Cost Center",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "undeposited_funds_account",
+   "fieldtype": "Link",
+   "hidden": 1,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Undeposited Funds Account",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Account",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 1,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-10-17 03:12:53.506229",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "QuickBooks Migrator",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "amend": 0,
+   "cancel": 0,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 0,
+   "if_owner": 0,
+   "import": 0,
+   "permlevel": 0,
+   "print": 1,
+   "read": 1,
+   "report": 0,
+   "role": "System Manager",
+   "set_user_permissions": 0,
+   "share": 1,
+   "submit": 0,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 0,
+ "track_seen": 0,
+ "track_views": 0
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
new file mode 100644
index 0000000..29a1a2b
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
@@ -0,0 +1,1274 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from requests_oauthlib import OAuth2Session
+import json, requests
+from erpnext import encode_company_abbr
+
+# QuickBooks requires a redirect URL, User will be redirect to this URL
+# This will be a GET request
+# Request parameters will have two parameters `code` and `realmId`
+# `code` is required to acquire refresh_token and access_token
+# `realmId` is the QuickBooks Company ID. It is Needed to actually fetch data.
+@frappe.whitelist()
+def callback(*args, **kwargs):
+	migrator = frappe.get_doc("QuickBooks Migrator")
+	migrator.set_indicator("Connecting to QuickBooks")
+	migrator.code = kwargs.get("code")
+	migrator.quickbooks_company_id = kwargs.get("realmId")
+	migrator.save()
+	migrator.get_tokens()
+	frappe.db.commit()
+	migrator.set_indicator("Connected to QuickBooks")
+	# We need this page to automatically close afterwards
+	frappe.respond_as_web_page("Quickbooks Authentication", html="<script>window.close()</script>")
+
+
+class QuickBooksMigrator(Document):
+	def __init__(self, *args, **kwargs):
+		super(QuickBooksMigrator, self).__init__(*args, **kwargs)
+		from pprint import pprint
+		self.oauth = OAuth2Session(
+			client_id=self.client_id,
+			redirect_uri=self.redirect_url,
+			scope=self.scope
+		)
+		if not self.authorization_url and self.authorization_endpoint:
+			self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0]
+
+
+	def on_update(self):
+		if self.company:
+			# We need a Cost Center corresponding to the selected erpnext Company
+			self.default_cost_center = frappe.db.get_value('Company', self.company, 'cost_center')
+			self.default_warehouse = frappe.get_all('Warehouse', filters={"company": self.company, "is_group": 0})[0]["name"]
+		if self.authorization_endpoint:
+			self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0]
+
+
+	def migrate(self):
+		frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long")
+
+
+	def _migrate(self):
+		try:
+			self.set_indicator("In Progress")
+			# Add quickbooks_id field to every document so that we can lookup by Id reference
+			# provided by documents in API responses.
+			# Also add a company field to Customer Supplier and Item
+			self._make_custom_fields()
+
+			self._migrate_accounts()
+
+			# Some Quickbooks Entities like Advance Payment, Payment aren't available firectly from API
+			# Sales Invoice also sometimes needs to be saved as a Journal Entry
+			# (When Item table is not present, This appens when Invoice is attached with a "StatementCharge" "ReimburseCharge
+			# Details of both of these cannot be fetched from API)
+			# Their GL entries need to be generated from GeneralLedger Report.
+			self._fetch_general_ledger()
+
+			# QuickBooks data can have transactions that do not fall in existing fiscal years in ERPNext
+			self._create_fiscal_years()
+
+			self._allow_fraction_in_unit()
+
+			# Following entities are directly available from API
+			# Invoice can be an exception sometimes though (as explained above).
+			entities_for_normal_transform = [
+				"Customer", "Item", "Vendor",
+				"Preferences",
+				"JournalEntry", "Purchase", "Deposit",
+				"Invoice", "CreditMemo", "SalesReceipt", "RefundReceipt",
+				"Bill", "VendorCredit",
+				"Payment", "BillPayment",
+			]
+			for entity in entities_for_normal_transform:
+				self._migrate_entries(entity)
+
+			# Following entries are not available directly from API, Need to be regenrated from GeneralLedger Report
+			entities_for_gl_transform = ["Advance Payment", "Tax Payment", "Sales Tax Payment", "Purchase Tax Payment", "Inventory Qty Adjust"]
+			for entity in entities_for_gl_transform:
+				self._migrate_entries_from_gl(entity)
+			self.set_indicator("Complete")
+		except Exception as e:
+			self.set_indicator("Failed")
+			self._log_error(e)
+
+		frappe.db.commit()
+
+
+	def get_tokens(self):
+		token = self.oauth.fetch_token(
+			token_url=self.token_endpoint,
+			client_secret=self.client_secret,
+			code=self.code
+		)
+		self.access_token = token["access_token"]
+		self.refresh_token = token["refresh_token"]
+		self.save()
+
+
+	def _refresh_tokens(self):
+		token = self.oauth.refresh_token(
+			token_url=self.token_endpoint,
+			client_id=self.client_id,
+			refresh_token=self.refresh_token,
+			client_secret=self.client_secret,
+			code=self.code
+		)
+		self.access_token = token["access_token"]
+		self.refresh_token = token["refresh_token"]
+		self.save()
+
+
+	def _make_custom_fields(self):
+		doctypes_for_quickbooks_id_field = ["Account", "Customer", "Address", "Item", "Supplier", "Sales Invoice", "Journal Entry", "Purchase Invoice"]
+		for doctype in doctypes_for_quickbooks_id_field:
+			self._make_custom_quickbooks_id_field(doctype)
+
+		doctypes_for_company_field = ["Customer", "Item", "Supplier"]
+		for doctype in doctypes_for_company_field:
+			self._make_custom_company_field(doctype)
+
+		frappe.db.commit()
+
+
+	def _make_custom_quickbooks_id_field(self, doctype):
+		if not frappe.get_meta(doctype).has_field("quickbooks_id"):
+			frappe.get_doc({
+				"doctype": "Custom Field",
+				"label": "QuickBooks ID",
+				"dt": doctype,
+				"fieldname": "quickbooks_id",
+				"fieldtype": "Data",
+			}).insert()
+
+
+	def _make_custom_company_field(self, doctype):
+		if not frappe.get_meta(doctype).has_field("company"):
+			frappe.get_doc({
+				"doctype": "Custom Field",
+				"label": "Company",
+				"dt": doctype,
+				"fieldname": "company",
+				"fieldtype": "Link",
+				"options": "Company",
+			}).insert()
+
+
+	def _migrate_accounts(self):
+		self._make_root_accounts()
+		for entity in ["Account", "TaxRate", "TaxCode"]:
+			self._migrate_entries(entity)
+
+
+	def _make_root_accounts(self):
+		roots = ["Asset", "Equity", "Expense", "Liability", "Income"]
+		for root in roots:
+			try:
+				if not frappe.db.exists({"doctype": "Account", "name": encode_company_abbr("{} - QB".format(root), self.company), "company": self.company}):
+					frappe.get_doc({
+						"doctype": "Account",
+						"account_name": "{} - QB".format(root),
+						"root_type": root,
+						"is_group": "1",
+						"company": self.company,
+					}).insert(ignore_mandatory=True)
+			except Exception as e:
+				self._log_error(e, root)
+		frappe.db.commit()
+
+
+	def _migrate_entries(self, entity):
+		try:
+			query_uri = "{}/company/{}/query".format(
+				self.api_endpoint,
+				self.quickbooks_company_id,
+			)
+			max_result_count = 1000
+			# Count number of entries
+			response = self._get(query_uri,
+				params={
+					"query": """SELECT COUNT(*) FROM {}""".format(entity)
+				}
+			)
+			entry_count = response.json()["QueryResponse"]["totalCount"]
+
+			# fetch pages and accumulate
+			entries = []
+			for start_position in range(1, entry_count + 1, max_result_count):
+				response = self._get(query_uri,
+					params={
+						"query": """SELECT * FROM {} STARTPOSITION {} MAXRESULTS {}""".format(
+							entity, start_position, max_result_count
+						)
+					}
+				)
+				entries.extend(response.json()["QueryResponse"][entity])
+			entries = self._preprocess_entries(entity, entries)
+			self._save_entries(entity, entries)
+		except Exception as e:
+			self._log_error(e, response.text)
+
+
+	def _fetch_general_ledger(self):
+		try:
+			query_uri = "{}/company/{}/reports/GeneralLedger".format(self.api_endpoint ,self.quickbooks_company_id)
+			response = self._get(query_uri,
+				params={
+					"columns": ",".join(["tx_date", "txn_type", "credit_amt", "debt_amt"]),
+					"date_macro": "All",
+					"minorversion": 3,
+				}
+			)
+			self.gl_entries = {}
+			for section in response.json()["Rows"]["Row"]:
+				if section["type"] == "Section":
+					self._get_gl_entries_from_section(section)
+			self.general_ledger = {}
+			for account in self.gl_entries.values():
+				for line in account:
+					type_dict = self.general_ledger.setdefault(line["type"], {})
+					if line["id"] not in type_dict:
+						type_dict[line["id"]] = {
+							"id": line["id"],
+							"date": line["date"],
+							"lines": [],
+						}
+					type_dict[line["id"]]["lines"].append(line)
+		except Exception as e:
+			self._log_error(e, response.text)
+
+
+	def _create_fiscal_years(self):
+		try:
+			# Assumes that exactly one fiscal year has been created so far
+			# Creates fiscal years till oldest ledger entry date is covered
+			from frappe.utils.data import add_years, getdate
+			from itertools import chain
+			smallest_ledger_entry_date = getdate(min(entry["date"] for entry in chain(*self.gl_entries.values()) if entry["date"]))
+			oldest_fiscal_year = frappe.get_all("Fiscal Year",
+				fields=["year_start_date", "year_end_date"],
+				order_by="year_start_date"
+			)[0]
+			# Keep on creating fiscal years
+			# until smallest_ledger_entry_date is no longer smaller than the oldest fiscal year's start date
+			while smallest_ledger_entry_date < oldest_fiscal_year.year_start_date:
+				new_fiscal_year = frappe.get_doc({"doctype": "Fiscal Year"})
+				new_fiscal_year.year_start_date = add_years(oldest_fiscal_year.year_start_date, -1)
+				new_fiscal_year.year_end_date = add_years(oldest_fiscal_year.year_end_date, -1)
+				if new_fiscal_year.year_start_date.year == new_fiscal_year.year_end_date.year:
+					new_fiscal_year.year = new_fiscal_year.year_start_date.year
+				else:
+					new_fiscal_year.year = "{}-{}".format(new_fiscal_year.year_start_date.year, new_fiscal_year.year_end_date.year)
+				new_fiscal_year.save()
+				oldest_fiscal_year = new_fiscal_year
+
+			frappe.db.commit()
+		except Exception as e:
+			self._log_error(e)
+
+
+	def _migrate_entries_from_gl(self, entity):
+		if entity in self.general_ledger:
+			self._save_entries(entity, self.general_ledger[entity].values())
+
+
+	def _save_entries(self, entity, entries):
+		entity_method_map = {
+			"Account": self._save_account,
+			"TaxRate": self._save_tax_rate,
+			"TaxCode": self._save_tax_code,
+
+			"Preferences": self._save_preference,
+
+			"Customer": self._save_customer,
+			"Item": self._save_item,
+			"Vendor": self._save_vendor,
+
+			"Invoice": self._save_invoice,
+			"CreditMemo": self._save_credit_memo,
+			"SalesReceipt": self._save_sales_receipt,
+			"RefundReceipt": self._save_refund_receipt,
+
+			"JournalEntry": self._save_journal_entry,
+
+			"Bill": self._save_bill,
+			"VendorCredit": self._save_vendor_credit,
+
+			"Payment": self._save_payment,
+			"BillPayment": self._save_bill_payment,
+
+			"Purchase": self._save_purchase,
+			"Deposit": self._save_deposit,
+
+			"Advance Payment": self._save_advance_payment,
+			"Tax Payment": self._save_tax_payment,
+			"Sales Tax Payment": self._save_tax_payment,
+			"Purchase Tax Payment": self._save_tax_payment,
+			"Inventory Qty Adjust": self._save_inventory_qty_adjust,
+		}
+		total = len(entries)
+		for index, entry in enumerate(entries, start=1):
+			self._publish({"event": "progress", "message": _("Saving {0}").format(entity), "count": index, "total": total})
+			entity_method_map[entity](entry)
+		frappe.db.commit()
+
+
+	def _preprocess_entries(self, entity, entries):
+		entity_method_map = {
+			"Account": self._preprocess_accounts,
+			"TaxRate": self._preprocess_tax_rates,
+			"TaxCode": self._preprocess_tax_codes,
+		}
+		preprocessor = entity_method_map.get(entity)
+		if preprocessor:
+			entries = preprocessor(entries)
+		return entries
+
+
+	def _get_gl_entries_from_section(self, section, account=None):
+		if "Header" in section:
+			if "id" in section["Header"]["ColData"][0]:
+				account = self._get_account_name_by_id(section["Header"]["ColData"][0]["id"])
+			elif "value" in section["Header"]["ColData"][0] and section["Header"]["ColData"][0]["value"]:
+				# For some reason during migrating UK company, account id is not available.
+				# preprocess_accounts retains name:account mapping in self.accounts
+				# This mapping can then be used to obtain quickbooks_id for correspondong account
+				# Rest is trivial
+
+				# Some Lines in General Leder Report are shown under Not Specified
+				# These should be skipped
+				if section["Header"]["ColData"][0]["value"] == "Not Specified":
+					return
+				account_id = self.accounts[section["Header"]["ColData"][0]["value"]]["Id"]
+				account = self._get_account_name_by_id(account_id)
+		entries = []
+		for row in section["Rows"]["Row"]:
+			if row["type"] == "Data":
+				data = row["ColData"]
+				entries.append({
+					"account": account,
+					"date": data[0]["value"],
+					"type": data[1]["value"],
+					"id": data[1].get("id"),
+					"credit": frappe.utils.flt(data[2]["value"]),
+					"debit": frappe.utils.flt(data[3]["value"]),
+				})
+			if row["type"] == "Section":
+				self._get_gl_entries_from_section(row, account)
+		self.gl_entries.setdefault(account, []).extend(entries)
+
+
+	def _preprocess_accounts(self, accounts):
+		self.accounts = {account["Name"]: account for account in accounts}
+		for account in accounts:
+			if any(acc["SubAccount"] and acc["ParentRef"]["value"] == account["Id"] for acc in accounts):
+				account["is_group"] = 1
+			else:
+				account["is_group"] = 0
+		return sorted(accounts, key=lambda account: int(account["Id"]))
+
+
+	def _save_account(self, account):
+		mapping = {
+			"Bank": "Asset",
+			"Other Current Asset": "Asset",
+			"Fixed Asset": "Asset",
+			"Other Asset": "Asset",
+			"Accounts Receivable": "Asset",
+
+			"Equity": "Equity",
+
+			"Expense": "Expense",
+			"Other Expense": "Expense",
+			"Cost of Goods Sold": "Expense",
+
+			"Accounts Payable": "Liability",
+			"Credit Card": "Liability",
+			"Long Term Liability": "Liability",
+			"Other Current Liability": "Liability",
+
+			"Income": "Income",
+			"Other Income": "Income",
+		}
+		# Map Quickbooks Account Types to ERPNext root_accunts and and root_type
+		try:
+			if not frappe.db.exists({"doctype": "Account", "quickbooks_id": account["Id"], "company": self.company}):
+				is_child = account["SubAccount"]
+				is_group = account["is_group"]
+				# Create Two Accounts for every Group Account
+				if is_group:
+					account_id = "Group - {}".format(account["Id"])
+				else:
+					account_id = account["Id"]
+
+				if is_child:
+					parent_account = self._get_account_name_by_id("Group - {}".format(account["ParentRef"]["value"]))
+				else:
+					parent_account = encode_company_abbr("{} - QB".format(mapping[account["AccountType"]]), self.company)
+
+				frappe.get_doc({
+					"doctype": "Account",
+					"quickbooks_id": account_id,
+					"account_name": self._get_unique_account_name(account["Name"]),
+					"root_type": mapping[account["AccountType"]],
+					"account_type": self._get_account_type(account),
+					"account_currency": account["CurrencyRef"]["value"],
+					"parent_account": parent_account,
+					"is_group": is_group,
+					"company": self.company,
+				}).insert()
+
+				if is_group:
+					# Create a Leaf account corresponding to the group account
+					frappe.get_doc({
+						"doctype": "Account",
+						"quickbooks_id": account["Id"],
+						"account_name": self._get_unique_account_name(account["Name"]),
+						"root_type": mapping[account["AccountType"]],
+						"account_type": self._get_account_type(account),
+						"account_currency": account["CurrencyRef"]["value"],
+						"parent_account": self._get_account_name_by_id(account_id),
+						"is_group": 0,
+						"company": self.company,
+					}).insert()
+				if account.get("AccountSubType") == "UndepositedFunds":
+					self.undeposited_funds_account = self._get_account_name_by_id(account["Id"])
+					self.save()
+		except Exception as e:
+			self._log_error(e, account)
+
+
+	def _get_account_type(self, account):
+		account_subtype_mapping = {"UndepositedFunds": "Cash"}
+		account_type = account_subtype_mapping.get(account.get("AccountSubType"))
+		if account_type is None:
+			account_type_mapping = {"Accounts Payable": "Payable", "Accounts Receivable": "Receivable", "Bank": "Bank", "Credit Card": "Bank"}
+			account_type = account_type_mapping.get(account["AccountType"])
+		return account_type
+
+
+	def _preprocess_tax_rates(self, tax_rates):
+		self.tax_rates = {tax_rate["Id"]: tax_rate for tax_rate in tax_rates}
+		return tax_rates
+
+
+	def _save_tax_rate(self, tax_rate):
+		try:
+			if not frappe.db.exists({"doctype": "Account", "quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]), "company": self.company}):
+				frappe.get_doc({
+					"doctype": "Account",
+					"quickbooks_id": "TaxRate - {}".format(tax_rate["Id"]),
+					"account_name": "{} - QB".format(tax_rate["Name"]),
+					"root_type": "Liability",
+					"parent_account": encode_company_abbr("{} - QB".format("Liability"), self.company),
+					"is_group": "0",
+					"company": self.company,
+				}).insert()
+		except Exception as e:
+			self._log_error(e, tax_rate)
+
+
+	def _preprocess_tax_codes(self, tax_codes):
+		self.tax_codes = {tax_code["Id"]: tax_code for tax_code in tax_codes}
+		return tax_codes
+
+
+	def _save_tax_code(self, tax_code):
+		pass
+
+
+	def _save_customer(self, customer):
+		try:
+			if not frappe.db.exists({"doctype": "Customer", "quickbooks_id": customer["Id"], "company": self.company}):
+				try:
+					receivable_account = frappe.get_all("Account", filters={
+						"account_type": "Receivable",
+						"account_currency": customer["CurrencyRef"]["value"],
+						"company": self.company,
+					})[0]["name"]
+				except Exception as e:
+					receivable_account = None
+				erpcustomer = frappe.get_doc({
+					"doctype": "Customer",
+					"quickbooks_id": customer["Id"],
+					"customer_name" : encode_company_abbr(customer["DisplayName"], self.company),
+					"customer_type" : "Individual",
+					"customer_group" : "Commercial",
+					"default_currency": customer["CurrencyRef"]["value"],
+					"accounts": [{"company": self.company, "account": receivable_account}],
+					"territory" : "All Territories",
+					"company": self.company,
+				}).insert()
+				if "BillAddr" in customer:
+					self._create_address(erpcustomer, "Customer", customer["BillAddr"], "Billing")
+				if "ShipAddr" in customer:
+					self._create_address(erpcustomer, "Customer", customer["ShipAddr"], "Shipping")
+		except Exception as e:
+			self._log_error(e, customer)
+
+
+	def _save_item(self, item):
+		try:
+			if not frappe.db.exists({"doctype": "Item", "quickbooks_id": item["Id"], "company": self.company}):
+				if item["Type"] in ("Service", "Inventory"):
+					item_dict = {
+						"doctype": "Item",
+						"quickbooks_id": item["Id"],
+						"item_code" : encode_company_abbr(item["Name"], self.company),
+						"stock_uom": "Unit",
+						"is_stock_item": 0,
+						"item_group": "All Item Groups",
+						"company": self.company,
+						"item_defaults": [{"company": self.company, "default_warehouse": self.default_warehouse}]
+					}
+					if "ExpenseAccountRef" in item:
+						expense_account = self._get_account_name_by_id(item["ExpenseAccountRef"]["value"])
+						item_dict["item_defaults"][0]["expense_account"] = expense_account
+					if "IncomeAccountRef" in item:
+						income_account = self._get_account_name_by_id(item["IncomeAccountRef"]["value"])
+						item_dict["item_defaults"][0]["income_account"] = income_account
+					frappe.get_doc(item_dict).insert()
+		except Exception as e:
+			self._log_error(e, item)
+
+
+	def _allow_fraction_in_unit(self):
+		frappe.db.set_value("UOM", "Unit", "must_be_whole_number", 0)
+
+
+	def _save_vendor(self, vendor):
+		try:
+			if not frappe.db.exists({"doctype": "Supplier", "quickbooks_id": vendor["Id"], "company": self.company}):
+				erpsupplier = frappe.get_doc({
+					"doctype": "Supplier",
+					"quickbooks_id": vendor["Id"],
+					"supplier_name" : encode_company_abbr(vendor["DisplayName"], self.company),
+					"supplier_group" : "All Supplier Groups",
+					"company": self.company,
+				}).insert()
+				if "BillAddr" in vendor:
+					self._create_address(erpsupplier, "Supplier", vendor["BillAddr"], "Billing")
+				if "ShipAddr" in vendor:
+					self._create_address(erpsupplier, "Supplier",vendor["ShipAddr"], "Shipping")
+		except Exception as e:
+			self._log_error(e)
+
+
+	def _save_preference(self, preference):
+		try:
+			if preference["SalesFormsPrefs"]["AllowShipping"]:
+				default_shipping_account_id = preference["SalesFormsPrefs"]["DefaultShippingAccount"]
+				self.default_shipping_account = self._get_account_name_by_id(self, default_shipping_account_id)
+				self.save()
+		except Exception as e:
+			self._log_error(e, preference)
+
+
+	def _save_invoice(self, invoice):
+		# Invoice can be Linked with Another Transactions
+		# If any of these transactions is a "StatementCharge" or "ReimburseCharge" then in the UI
+		# item list is populated from the corresponding transaction, these items are not shown in api response
+		# Also as of now there is no way of fetching the corresponding transaction from api
+		# We in order to correctly reflect account balance make an equivalent Journal Entry
+		quickbooks_id = "Invoice - {}".format(invoice["Id"])
+		if any(linked["TxnType"] in ("StatementCharge", "ReimburseCharge") for linked in invoice["LinkedTxn"]):
+			self._save_invoice_as_journal_entry(invoice, quickbooks_id)
+		else:
+			self._save_sales_invoice(invoice, quickbooks_id)
+
+
+	def _save_credit_memo(self, credit_memo):
+		# Credit Memo is equivalent to a return Sales Invoice
+		quickbooks_id = "Credit Memo - {}".format(credit_memo["Id"])
+		self._save_sales_invoice(credit_memo, quickbooks_id, is_return=True)
+
+
+	def _save_sales_receipt(self, sales_receipt):
+		# Sales Receipt is equivalent to a POS Sales Invoice
+		quickbooks_id = "Sales Receipt - {}".format(sales_receipt["Id"])
+		self._save_sales_invoice(sales_receipt, quickbooks_id, is_pos=True)
+
+
+	def _save_refund_receipt(self, refund_receipt):
+		# Refund Receipt is equivalent to a return POS Sales Invoice
+		quickbooks_id = "Refund Receipt - {}".format(refund_receipt["Id"])
+		self._save_sales_invoice(refund_receipt, quickbooks_id, is_return=True, is_pos=True)
+
+
+	def _save_sales_invoice(self, invoice, quickbooks_id, is_return=False, is_pos=False):
+		try:
+			if not frappe.db.exists({"doctype": "Sales Invoice", "quickbooks_id": quickbooks_id, "company": self.company}):
+				invoice_dict = {
+					"doctype": "Sales Invoice",
+					"quickbooks_id": quickbooks_id,
+
+					# Quickbooks uses ISO 4217 Code
+					# of course this gonna come back to bite me
+					"currency": invoice["CurrencyRef"]["value"],
+
+					# Exchange Rate is provided if multicurrency is enabled
+					# It is not provided if multicurrency is not enabled
+					"conversion_rate": invoice.get("ExchangeRate", 1),
+					"posting_date": invoice["TxnDate"],
+
+					# QuickBooks doesn't make Due Date a mandatory field this is a hack
+					"due_date": invoice.get("DueDate", invoice["TxnDate"]),
+					"customer": frappe.get_all("Customer",
+						filters={
+							"quickbooks_id": invoice["CustomerRef"]["value"],
+							"company": self.company,
+						})[0]["name"],
+					"items": self._get_si_items(invoice, is_return=is_return),
+					"taxes": self._get_taxes(invoice),
+
+					# Do not change posting_date upon submission
+					"set_posting_time": 1,
+
+					# QuickBooks doesn't round total
+					"disable_rounded_total": 1,
+					"is_return": is_return,
+					"is_pos": is_pos,
+					"payments": self._get_invoice_payments(invoice, is_return=is_return, is_pos=is_pos),
+					"company": self.company,
+				}
+				discount = self._get_discount(invoice["Line"])
+				if discount:
+					if invoice["ApplyTaxAfterDiscount"]:
+						invoice_dict["apply_discount_on"] = "Net Total"
+					else:
+						invoice_dict["apply_discount_on"] = "Grand Total"
+					invoice_dict["discount_amount"] = discount["Amount"]
+
+				invoice_doc = frappe.get_doc(invoice_dict)
+				invoice_doc.insert()
+				invoice_doc.submit()
+		except Exception as e:
+			self._log_error(e, [invoice, invoice_dict, json.loads(invoice_doc.as_json())])
+
+
+	def _get_si_items(self, invoice, is_return=False):
+		items = []
+		for line in invoice["Line"]:
+			if line["DetailType"] == "SalesItemLineDetail":
+				if line["SalesItemLineDetail"]["TaxCodeRef"]["value"] != "TAX":
+					tax_code = line["SalesItemLineDetail"]["TaxCodeRef"]["value"]
+				else:
+					if "TxnTaxCodeRef" in invoice["TxnTaxDetail"]:
+						tax_code = invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"]
+					else:
+						tax_code = "NON"
+				if line["SalesItemLineDetail"]["ItemRef"]["value"] != "SHIPPING_ITEM_ID":
+					item = frappe.db.get_all("Item",
+						filters={
+							"quickbooks_id": line["SalesItemLineDetail"]["ItemRef"]["value"],
+							"company": self.company,
+						},
+						fields=["name", "stock_uom"]
+					)[0]
+					items.append({
+						"item_code": item["name"],
+						"conversion_factor": 1,
+						"uom": item["stock_uom"],
+						"description": line.get("Description", line["SalesItemLineDetail"]["ItemRef"]["name"]),
+						"qty": line["SalesItemLineDetail"]["Qty"],
+						"price_list_rate": line["SalesItemLineDetail"]["UnitPrice"],
+						"cost_center": self.default_cost_center,
+						"warehouse": self.default_warehouse,
+						"item_tax_rate": json.dumps(self._get_item_taxes(tax_code))
+					})
+				else:
+					items.append({
+						"item_name": "Shipping",
+						"conversion_factor": 1,
+						"expense_account": self._get_account_name_by_id("TaxRate - {}".format(line["SalesItemLineDetail"]["TaxCodeRef"]["value"])),
+						"uom": "Unit",
+						"description": "Shipping",
+						"income_account": self.default_shipping_account,
+						"qty": 1,
+						"price_list_rate": line["Amount"],
+						"cost_center": self.default_cost_center,
+						"warehouse": self.default_warehouse,
+						"item_tax_rate": json.dumps(self._get_item_taxes(tax_code))
+					})
+				if is_return:
+					items[-1]["qty"] *= -1
+			elif line["DetailType"] == "DescriptionOnly":
+				items[-1].update({
+					"margin_type": "Percentage",
+					"margin_rate_or_amount": int(line["Description"].split("%")[0]),
+				})
+		return items
+
+
+	def _get_item_taxes(self, tax_code):
+		tax_rates = self.tax_rates
+		item_taxes = {}
+		if tax_code != "NON":
+			tax_code = self.tax_codes[tax_code]
+			for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"):
+				if rate_list_type in tax_code:
+					for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]:
+						if tax_rate_detail["TaxTypeApplicable"] == "TaxOnAmount":
+							tax_head = self._get_account_name_by_id("TaxRate - {}".format(tax_rate_detail["TaxRateRef"]["value"]))
+							tax_rate = tax_rates[tax_rate_detail["TaxRateRef"]["value"]]
+							item_taxes[tax_head] = tax_rate["RateValue"]
+		return item_taxes
+
+
+	def _get_invoice_payments(self, invoice, is_return=False, is_pos=False):
+		if is_pos:
+			amount = invoice["TotalAmt"]
+			if is_return:
+				amount = -amount
+			return [{
+				"mode_of_payment": "Cash",
+				"account": self._get_account_name_by_id(invoice["DepositToAccountRef"]["value"]),
+				"amount": amount,
+			}]
+
+
+	def _get_discount(self, lines):
+		for line in lines:
+			if line["DetailType"] == "DiscountLineDetail" and "Amount" in line["DiscountLineDetail"]:
+				return line
+
+
+	def _save_invoice_as_journal_entry(self, invoice, quickbooks_id):
+		try:
+			accounts = []
+			for line in self.general_ledger["Invoice"][invoice["Id"]]["lines"]:
+				account_line = {"account": line["account"], "cost_center": self.default_cost_center}
+				if line["debit"]:
+					account_line["debit_in_account_currency"] = line["debit"]
+				elif line["credit"]:
+					account_line["credit_in_account_currency"] = line["credit"]
+				if frappe.db.get_value("Account", line["account"], "account_type") == "Receivable":
+					account_line["party_type"] = "Customer"
+					account_line["party"] = frappe.get_all("Customer",
+						filters={"quickbooks_id": invoice["CustomerRef"]["value"], "company": self.company}
+					)[0]["name"]
+
+				accounts.append(account_line)
+
+			posting_date = invoice["TxnDate"]
+			self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+		except Exception as e:
+			self._log_error(e, [invoice, accounts])
+
+
+	def _save_journal_entry(self, journal_entry):
+		# JournalEntry is equivalent to a Journal Entry
+
+		def _get_je_accounts(lines):
+			# Converts JounalEntry lines to accounts list
+			posting_type_field_mapping = {
+				"Credit": "credit_in_account_currency",
+				"Debit": "debit_in_account_currency",
+			}
+			accounts = []
+			for line in lines:
+				if line["DetailType"] == "JournalEntryLineDetail":
+					account_name = self._get_account_name_by_id(line["JournalEntryLineDetail"]["AccountRef"]["value"])
+					posting_type = line["JournalEntryLineDetail"]["PostingType"]
+					accounts.append({
+						"account": account_name,
+						posting_type_field_mapping[posting_type]: line["Amount"],
+						"cost_center": self.default_cost_center,
+					})
+			return accounts
+
+		quickbooks_id = "Journal Entry - {}".format(journal_entry["Id"])
+		accounts = _get_je_accounts(journal_entry["Line"])
+		posting_date = journal_entry["TxnDate"]
+		self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+
+
+	def __save_journal_entry(self, quickbooks_id, accounts, posting_date):
+		try:
+			if not frappe.db.exists({"doctype": "Journal Entry", "quickbooks_id": quickbooks_id, "company": self.company}):
+				je = frappe.get_doc({
+					"doctype": "Journal Entry",
+					"quickbooks_id": quickbooks_id,
+					"company": self.company,
+					"posting_date": posting_date,
+					"accounts": accounts,
+					"multi_currency": 1,
+				})
+				je.insert()
+				je.submit()
+		except Exception as e:
+			self._log_error(e, [accounts, json.loads(je.as_json())])
+
+
+	def _save_bill(self, bill):
+		# Bill is equivalent to a Purchase Invoice
+		quickbooks_id = "Bill - {}".format(bill["Id"])
+		self.__save_purchase_invoice(bill, quickbooks_id)
+
+
+	def _save_vendor_credit(self, vendor_credit):
+		# Vendor Credit is equivalent to a return Purchase Invoice
+		quickbooks_id = "Vendor Credit - {}".format(vendor_credit["Id"])
+		self.__save_purchase_invoice(vendor_credit, quickbooks_id, is_return=True)
+
+
+	def __save_purchase_invoice(self, invoice, quickbooks_id, is_return=False):
+		try:
+			if not frappe.db.exists({"doctype": "Purchase Invoice", "quickbooks_id": quickbooks_id, "company": self.company}):
+				credit_to_account = self._get_account_name_by_id(invoice["APAccountRef"]["value"])
+				invoice_dict = {
+					"doctype": "Purchase Invoice",
+					"quickbooks_id": quickbooks_id,
+					"currency": invoice["CurrencyRef"]["value"],
+					"conversion_rate": invoice.get("ExchangeRate", 1),
+					"posting_date": invoice["TxnDate"],
+					"due_date":  invoice.get("DueDate", invoice["TxnDate"]),
+					"credit_to": credit_to_account,
+					"supplier": frappe.get_all("Supplier",
+						filters={
+							"quickbooks_id": invoice["VendorRef"]["value"],
+							"company": self.company,
+						})[0]["name"],
+					"items": self._get_pi_items(invoice, is_return=is_return),
+					"taxes": self._get_taxes(invoice),
+					"set_posting_time": 1,
+					"disable_rounded_total": 1,
+					"is_return": is_return,
+					"udpate_stock": 0,
+					"company": self.company,
+				}
+				invoice_doc = frappe.get_doc(invoice_dict)
+				invoice_doc.insert()
+				invoice_doc.submit()
+		except Exception as e:
+			self._log_error(e, [invoice, invoice_dict, json.loads(invoice_doc.as_json())])
+
+
+	def _get_pi_items(self, purchase_invoice, is_return=False):
+		items = []
+		for line in purchase_invoice["Line"]:
+			if line["DetailType"] == "ItemBasedExpenseLineDetail":
+				if line["ItemBasedExpenseLineDetail"]["TaxCodeRef"]["value"] != "TAX":
+					tax_code = line["ItemBasedExpenseLineDetail"]["TaxCodeRef"]["value"]
+				else:
+					if "TxnTaxCodeRef" in purchase_invoice["TxnTaxDetail"]:
+						tax_code = purchase_invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"]
+					else:
+						tax_code = "NON"
+				item = frappe.db.get_all("Item",
+					filters={
+						"quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"],
+						"company": self.company
+					},
+					fields=["name", "stock_uom"]
+				)[0]
+				items.append({
+					"item_code": item["name"],
+					"conversion_factor": 1,
+					"uom": item["stock_uom"],
+					"description": line.get("Description", line["ItemBasedExpenseLineDetail"]["ItemRef"]["name"]),
+					"qty": line["ItemBasedExpenseLineDetail"]["Qty"],
+					"price_list_rate": line["ItemBasedExpenseLineDetail"]["UnitPrice"],
+					"warehouse": self.default_warehouse,
+					"cost_center": self.default_cost_center,
+					"item_tax_rate": json.dumps(self._get_item_taxes(tax_code)),
+				})
+			elif line["DetailType"] == "AccountBasedExpenseLineDetail":
+				if line["AccountBasedExpenseLineDetail"]["TaxCodeRef"]["value"] != "TAX":
+					tax_code = line["AccountBasedExpenseLineDetail"]["TaxCodeRef"]["value"]
+				else:
+					if "TxnTaxCodeRef" in purchase_invoice["TxnTaxDetail"]:
+						tax_code = purchase_invoice["TxnTaxDetail"]["TxnTaxCodeRef"]["value"]
+					else:
+						tax_code = "NON"
+				items.append({
+					"item_name": line.get("Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"]),
+					"conversion_factor": 1,
+					"expense_account": self._get_account_name_by_id(line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"]),
+					"uom": "Unit",
+					"description": line.get("Description", line["AccountBasedExpenseLineDetail"]["AccountRef"]["name"]),
+					"qty": 1,
+					"price_list_rate": line["Amount"],
+					"warehouse": self.default_warehouse,
+					"cost_center": self.default_cost_center,
+					"item_tax_rate": json.dumps(self._get_item_taxes(tax_code)),
+				})
+			if is_return:
+				items[-1]["qty"] *= -1
+		return items
+
+
+	def _save_payment(self, payment):
+		try:
+			quickbooks_id = "Payment - {}".format(payment["Id"])
+			# If DepositToAccountRef is not set on payment that means it actually doesn't affect any accounts
+			# No need to record such payment
+			# Such payment record is created QuickBooks Payments API
+			if "DepositToAccountRef" not in payment:
+				return
+
+			# A Payment can be linked to multiple transactions
+			accounts = []
+			for line in payment["Line"]:
+				linked_transaction = line["LinkedTxn"][0]
+				if linked_transaction["TxnType"] == "Invoice":
+					si_quickbooks_id = "Invoice - {}".format(linked_transaction["TxnId"])
+					# Invoice could have been saved as a Sales Invoice or a Journal Entry
+					if frappe.db.exists({"doctype": "Sales Invoice", "quickbooks_id": si_quickbooks_id, "company": self.company}):
+						sales_invoice = frappe.get_all("Sales Invoice",
+							filters={
+								"quickbooks_id": si_quickbooks_id,
+								"company": self.company,
+							},
+							fields=["name", "customer", "debit_to"],
+						)[0]
+						reference_type = "Sales Invoice"
+						reference_name = sales_invoice["name"]
+						party = sales_invoice["customer"]
+						party_account = sales_invoice["debit_to"]
+
+					if frappe.db.exists({"doctype": "Journal Entry", "quickbooks_id": si_quickbooks_id, "company": self.company}):
+						journal_entry = frappe.get_doc("Journal Entry",
+							{
+								"quickbooks_id": si_quickbooks_id,
+								"company": self.company,
+							}
+						)
+						# Invoice saved as a Journal Entry must have party and party_type set on line containing Receivable Account
+						customer_account_line = list(filter(lambda acc: acc.party_type == "Customer", journal_entry.accounts))[0]
+
+						reference_type = "Journal Entry"
+						reference_name = journal_entry.name
+						party = customer_account_line.party
+						party_account = customer_account_line.account
+
+					accounts.append({
+						"party_type": "Customer",
+						"party": party,
+						"reference_type": reference_type,
+						"reference_name": reference_name,
+						"account": party_account,
+						"credit_in_account_currency": line["Amount"],
+						"cost_center": self.default_cost_center,
+					})
+
+			deposit_account = self._get_account_name_by_id(payment["DepositToAccountRef"]["value"])
+			accounts.append({
+				"account": deposit_account,
+				"debit_in_account_currency": payment["TotalAmt"],
+				"cost_center": self.default_cost_center,
+			})
+			posting_date = payment["TxnDate"]
+			self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+		except Exception as e:
+			self._log_error(e, [payment, accounts])
+
+
+	def _save_bill_payment(self, bill_payment):
+		try:
+			quickbooks_id = "BillPayment - {}".format(bill_payment["Id"])
+			# A BillPayment can be linked to multiple transactions
+			accounts = []
+			for line in bill_payment["Line"]:
+				linked_transaction = line["LinkedTxn"][0]
+				if linked_transaction["TxnType"] == "Bill":
+					pi_quickbooks_id = "Bill - {}".format(linked_transaction["TxnId"])
+					if frappe.db.exists({"doctype": "Purchase Invoice", "quickbooks_id": pi_quickbooks_id, "company": self.company}):
+						purchase_invoice = frappe.get_all("Purchase Invoice",
+							filters={
+								"quickbooks_id": pi_quickbooks_id,
+								"company": self.company,
+							},
+							fields=["name", "supplier", "credit_to"],
+						)[0]
+						reference_type = "Purchase Invoice"
+						reference_name = purchase_invoice["name"]
+						party = purchase_invoice["supplier"]
+						party_account = purchase_invoice["credit_to"]
+					accounts.append({
+						"party_type": "Supplier",
+						"party": party,
+						"reference_type": reference_type,
+						"reference_name": reference_name,
+						"account": party_account,
+						"debit_in_account_currency": line["Amount"],
+						"cost_center": self.default_cost_center,
+					})
+
+			if bill_payment["PayType"] == "Check":
+				bank_account_id = bill_payment["CheckPayment"]["BankAccountRef"]["value"]
+			elif bill_payment["PayType"] == "CreditCard":
+				bank_account_id = bill_payment["CreditCardPayment"]["CCAccountRef"]["value"]
+
+			bank_account = self._get_account_name_by_id(bank_account_id)
+			accounts.append({
+				"account": bank_account,
+				"credit_in_account_currency": bill_payment["TotalAmt"],
+				"cost_center": self.default_cost_center,
+			})
+			posting_date = bill_payment["TxnDate"]
+			self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+		except Exception as e:
+			self._log_error(e, [bill_payment, accounts])
+
+
+	def _save_purchase(self, purchase):
+		try:
+			quickbooks_id = "Purchase - {}".format(purchase["Id"])
+			# Credit Bank Account
+			accounts = [{
+				"account": self._get_account_name_by_id(purchase["AccountRef"]["value"]),
+				"credit_in_account_currency": purchase["TotalAmt"],
+				"cost_center": self.default_cost_center,
+			}]
+
+			# Debit Mentioned Accounts
+			for line in purchase["Line"]:
+				if line["DetailType"] == "AccountBasedExpenseLineDetail":
+					account = self._get_account_name_by_id(line["AccountBasedExpenseLineDetail"]["AccountRef"]["value"])
+				elif line["DetailType"] == "ItemBasedExpenseLineDetail":
+					account = frappe.get_doc("Item",
+						{"quickbooks_id": line["ItemBasedExpenseLineDetail"]["ItemRef"]["value"], "company": self.company}
+					).item_defaults[0].expense_account
+				accounts.append({
+					"account": account,
+					"debit_in_account_currency": line["Amount"],
+					"cost_center": self.default_cost_center,
+				})
+
+			# Debit Tax Accounts
+			if "TxnTaxDetail" in purchase:
+				for line in purchase["TxnTaxDetail"]["TaxLine"]:
+					accounts.append({
+						"account": self._get_account_name_by_id("TaxRate - {}".format(line["TaxLineDetail"]["TaxRateRef"]["value"])),
+						"debit_in_account_currency": line["Amount"],
+						"cost_center": self.default_cost_center,
+					})
+
+			# If purchase["Credit"] is set to be True then it represents a refund
+			if purchase.get("Credit"):
+				for account in accounts:
+					if "debit_in_account_currency" in account:
+						account["credit_in_account_currency"] = account["debit_in_account_currency"]
+						del account["debit_in_account_currency"]
+					else:
+						account["debit_in_account_currency"] = account["credit_in_account_currency"]
+						del account["credit_in_account_currency"]
+
+			posting_date = purchase["TxnDate"]
+			self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+		except Exception as e:
+			self._log_error(e, [purchase, accounts])
+
+
+	def _save_deposit(self, deposit):
+		try:
+			quickbooks_id = "Deposit - {}".format(deposit["Id"])
+			# Debit Bank Account
+			accounts = [{
+				"account": self._get_account_name_by_id(deposit["DepositToAccountRef"]["value"]),
+				"debit_in_account_currency": deposit["TotalAmt"],
+				"cost_center": self.default_cost_center,
+			}]
+
+			# Credit Mentioned Accounts
+			for line in deposit["Line"]:
+				if "LinkedTxn" in line:
+					accounts.append({
+						"account": self.undeposited_funds_account,
+						"credit_in_account_currency": line["Amount"],
+						"cost_center": self.default_cost_center,
+					})
+				else:
+					accounts.append({
+						"account": self._get_account_name_by_id(line["DepositLineDetail"]["AccountRef"]["value"]),
+						"credit_in_account_currency": line["Amount"],
+						"cost_center": self.default_cost_center,
+					})
+
+			# Debit Cashback if mentioned
+			if "CashBack" in deposit:
+				accounts.append({
+					"account": self._get_account_name_by_id(deposit["CashBack"]["AccountRef"]["value"]),
+					"debit_in_account_currency": deposit["CashBack"]["Amount"],
+					"cost_center": self.default_cost_center,
+				})
+
+			posting_date = deposit["TxnDate"]
+			self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+		except Exception as e:
+			self._log_error(e, [deposit, accounts])
+
+
+	def _save_advance_payment(self, advance_payment):
+		quickbooks_id = "Advance Payment - {}".format(advance_payment["id"])
+		self.__save_ledger_entry_as_je(advance_payment, quickbooks_id)
+
+
+	def _save_tax_payment(self, tax_payment):
+		quickbooks_id = "Tax Payment - {}".format(tax_payment["id"])
+		self.__save_ledger_entry_as_je(tax_payment, quickbooks_id)
+
+
+	def _save_inventory_qty_adjust(self, inventory_qty_adjust):
+		quickbooks_id = "Inventory Qty Adjust - {}".format(inventory_qty_adjust["id"])
+		self.__save_ledger_entry_as_je(inventory_qty_adjust, quickbooks_id)
+
+
+	def __save_ledger_entry_as_je(self, ledger_entry, quickbooks_id):
+		try:
+			accounts = []
+			for line in ledger_entry["lines"]:
+				account_line = {"account": line["account"], "cost_center": self.default_cost_center}
+				if line["credit"]:
+					account_line["credit_in_account_currency"] = line["credit"]
+				else:
+					account_line["debit_in_account_currency"] = line["debit"]
+				accounts.append(account_line)
+
+			posting_date = ledger_entry["date"]
+			self.__save_journal_entry(quickbooks_id, accounts, posting_date)
+		except Exception as e:
+			self._log_error(e, ledger_entry)
+
+
+	def _get_taxes(self, entry):
+		taxes = []
+		if "TxnTaxDetail" not in entry or "TaxLine" not in entry["TxnTaxDetail"]:
+			return taxes
+		for line in entry["TxnTaxDetail"]["TaxLine"]:
+			tax_rate = line["TaxLineDetail"]["TaxRateRef"]["value"]
+			account_head = self._get_account_name_by_id("TaxRate - {}".format(tax_rate))
+			tax_type_applicable = self._get_tax_type(tax_rate)
+			if tax_type_applicable == "TaxOnAmount":
+				taxes.append({
+					"charge_type": "On Net Total",
+					"account_head": account_head,
+					"description": account_head,
+					"cost_center": self.default_cost_center,
+					"rate": 0,
+				})
+			else:
+				parent_tax_rate = self._get_parent_tax_rate(tax_rate)
+				parent_row_id = self._get_parent_row_id(parent_tax_rate, taxes)
+				taxes.append({
+					"charge_type": "On Previous Row Amount",
+					"row_id": parent_row_id,
+					"account_head": account_head,
+					"description": account_head,
+					"cost_center": self.default_cost_center,
+					"rate": line["TaxLineDetail"]["TaxPercent"],
+				})
+		return taxes
+
+
+	def _get_tax_type(self, tax_rate):
+		for tax_code in self.tax_codes.values():
+			for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"):
+				if rate_list_type in tax_code:
+					for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]:
+						if tax_rate_detail["TaxRateRef"]["value"] == tax_rate:
+							return tax_rate_detail["TaxTypeApplicable"]
+
+
+	def _get_parent_tax_rate(self, tax_rate):
+		parent = None
+		for tax_code in self.tax_codes.values():
+			for rate_list_type in ("SalesTaxRateList", "PurchaseTaxRateList"):
+				if rate_list_type in tax_code:
+					for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]:
+						if tax_rate_detail["TaxRateRef"]["value"] == tax_rate:
+							parent = tax_rate_detail["TaxOnTaxOrder"]
+					if parent:
+						for tax_rate_detail in tax_code[rate_list_type]["TaxRateDetail"]:
+							if tax_rate_detail["TaxOrder"] == parent:
+								return tax_rate_detail["TaxRateRef"]["value"]
+
+
+	def _get_parent_row_id(self, tax_rate, taxes):
+		tax_account = self._get_account_name_by_id("TaxRate - {}".format(tax_rate))
+		for index, tax in enumerate(taxes):
+			if tax["account_head"] == tax_account:
+				return index + 1
+
+
+	def _create_address(self, entity, doctype, address, address_type):
+		try :
+			if not frappe.db.exists({"doctype": "Address", "quickbooks_id": address["Id"]}):
+				frappe.get_doc({
+					"doctype": "Address",
+					"quickbooks_address_id": address["Id"],
+					"address_title": entity.name,
+					"address_type": address_type,
+					"address_line1": address["Line1"],
+					"city": address["City"],
+					"links": [{"link_doctype": doctype, "link_name": entity.name}]
+				}).insert()
+		except Exception as e:
+			self._log_error(e, address)
+
+
+	def _get(self, *args, **kwargs):
+		kwargs["headers"] = {
+			"Accept": "application/json",
+			"Authorization": "Bearer {}".format(self.access_token)
+		}
+		response = requests.get(*args, **kwargs)
+		# HTTP Status code 401 here means that the access_token is expired
+		# We can refresh tokens and retry
+		# However limitless recursion does look dangerous
+		if response.status_code == 401:
+			self._refresh_tokens()
+			response = self._get(*args, **kwargs)
+		return response
+
+
+	def _get_account_name_by_id(self, quickbooks_id):
+		return frappe.get_all("Account", filters={"quickbooks_id": quickbooks_id, "company": self.company})[0]["name"]
+
+
+	def _publish(self, *args, **kwargs):
+		frappe.publish_realtime("quickbooks_progress_update", *args, **kwargs)
+
+
+	def _get_unique_account_name(self, quickbooks_name, number=0):
+		if number:
+			quickbooks_account_name = "{} - {} - QB".format(quickbooks_name, number)
+		else:
+			quickbooks_account_name = "{} - QB".format(quickbooks_name)
+		company_encoded_account_name = encode_company_abbr(quickbooks_account_name, self.company)
+		if frappe.db.exists({"doctype": "Account", "name": company_encoded_account_name, "company": self.company}):
+			unique_account_name = self._get_unique_account_name(quickbooks_name, number + 1)
+		else:
+			unique_account_name = quickbooks_account_name
+		return unique_account_name
+
+
+	def _log_error(self, execption, data=""):
+		import json, traceback
+		traceback.print_exc()
+		frappe.log_error(title="QuickBooks Migration Error",
+			message="\n".join([
+				"Data",
+				json.dumps(data,
+					sort_keys=True,
+					indent=4,
+					separators=(',', ': ')
+				),
+				"Exception",
+				traceback.format_exc()
+			])
+		)
+
+
+	def set_indicator(self, status):
+		self.status = status
+		self.save()
+		frappe.db.commit()
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.js b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.js
new file mode 100644
index 0000000..b71d704
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: QuickBooks Migrator", function (assert) {
+	let done = assert.async();
+
+	// number of asserts
+	assert.expect(1);
+
+	frappe.run_serially([
+		// insert a new QuickBooks Migrator
+		() => frappe.tests.make('QuickBooks Migrator', [
+			// values to be set
+			{key: 'value'}
+		]),
+		() => {
+			assert.equal(cur_frm.doc.key, 'value');
+		},
+		() => done()
+	]);
+
+});
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py
new file mode 100644
index 0000000..6ce7c92
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/test_quickbooks_migrator.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestQuickBooksMigrator(unittest.TestCase):
+	pass