Merge pull request #27497 from nemesis189/taxjar-nexus-update

fix: TaxJar update - added nexus list, making api call only for nexus
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py b/erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json
new file mode 100644
index 0000000..d4d4a51
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-09-11 05:09:53.773838",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "region",
+  "region_code",
+  "country",
+  "country_code"
+ ],
+ "fields": [
+  {
+   "fieldname": "region",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Region"
+  },
+  {
+   "fieldname": "region_code",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Region Code"
+  },
+  {
+   "fieldname": "country",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Country"
+  },
+  {
+   "fieldname": "country_code",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Country Code"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-14 05:33:06.444710",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "TaxJar Nexus",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py
new file mode 100644
index 0000000..c24aa8c
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TaxJarNexus(Document):
+	pass
diff --git a/erpnext/regional/united_states/product_tax_category_data.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/product_tax_category_data.json
similarity index 100%
rename from erpnext/regional/united_states/product_tax_category_data.json
rename to erpnext/erpnext_integrations/doctype/taxjar_settings/product_tax_category_data.json
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
index 62d5709..d495989 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
@@ -5,5 +5,16 @@
 	is_sandbox: (frm) => {
 		frm.toggle_reqd("api_key", !frm.doc.is_sandbox);
 		frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox);
-	}
+	},
+
+	refresh: (frm) => {
+		frm.add_custom_button(__('Update Nexus List'), function() {
+			frm.call({
+				doc: frm.doc,
+				method: 'update_nexus_list'
+			});
+		});
+	},
+
+
 });
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index c0d60f7..2d17f2e 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -6,8 +6,8 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "is_sandbox",
   "taxjar_calculate_tax",
+  "is_sandbox",
   "taxjar_create_transactions",
   "credentials",
   "api_key",
@@ -16,7 +16,10 @@
   "configuration",
   "tax_account_head",
   "configuration_cb",
-  "shipping_account_head"
+  "shipping_account_head",
+  "section_break_12",
+  "nexus_address",
+  "nexus"
  ],
  "fields": [
   {
@@ -54,6 +57,7 @@
   },
   {
    "default": "0",
+   "depends_on": "taxjar_calculate_tax",
    "fieldname": "is_sandbox",
    "fieldtype": "Check",
    "label": "Sandbox Mode"
@@ -69,6 +73,7 @@
   },
   {
    "default": "0",
+   "depends_on": "taxjar_calculate_tax",
    "fieldname": "taxjar_create_transactions",
    "fieldtype": "Check",
    "label": "Create TaxJar Transaction"
@@ -82,11 +87,28 @@
   {
    "fieldname": "cb_keys",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_12",
+   "fieldtype": "Section Break",
+   "label": "Nexus List"
+  },
+  {
+   "fieldname": "nexus_address",
+   "fieldtype": "HTML",
+   "label": "Nexus Address"
+  },
+  {
+   "fieldname": "nexus",
+   "fieldtype": "Table",
+   "label": "Nexus",
+   "options": "TaxJar Nexus",
+   "read_only": 1
   }
  ],
  "issingle": 1,
  "links": [],
- "modified": "2020-04-30 04:38:03.311089",
+ "modified": "2021-10-06 10:59:13.475442",
  "modified_by": "Administrator",
  "module": "ERPNext Integrations",
  "name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index 9dd4817..f430a9e 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -4,9 +4,98 @@
 
 from __future__ import unicode_literals
 
-# import frappe
+import json
+import os
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
 from frappe.model.document import Document
+from frappe.permissions import add_permission, update_permission_property
+
+from erpnext.erpnext_integrations.taxjar_integration import get_client
 
 
 class TaxJarSettings(Document):
-	pass
+
+	def on_update(self):
+		TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+		TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+		TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+
+		fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
+		fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
+
+		if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE):
+			if not fields_already_exist:
+				add_product_tax_categories()
+				make_custom_fields()
+				add_permissions()
+				frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
+
+			elif fields_already_exist and fields_hidden:
+				toggle_tax_category_fields(hidden='0')
+
+		elif fields_already_exist:
+			toggle_tax_category_fields(hidden='1')
+
+	def validate(self):
+		self.calculate_taxes_validation_for_create_transactions()
+
+	@frappe.whitelist()
+	def update_nexus_list(self):
+		client = get_client()
+		nexus = client.nexus_regions()
+
+		new_nexus_list = [frappe._dict(address) for address in nexus]
+
+		self.set('nexus', [])
+		self.set('nexus', new_nexus_list)
+		self.save()
+
+	def calculate_taxes_validation_for_create_transactions(self):
+		if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox):
+			frappe.throw(frappe._('Before enabling <b>Create Transaction</b> or <b>Sandbox Mode</b>, you need to check the <b>Enable Tax Calculation</b> box'))
+
+
+def toggle_tax_category_fields(hidden):
+	frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
+	frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
+
+
+def add_product_tax_categories():
+	with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
+		tax_categories = json.loads(f.read())
+	create_tax_categories(tax_categories['categories'])
+
+def create_tax_categories(data):
+	for d in data:
+		if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}):
+			tax_category = frappe.new_doc('Product Tax Category')
+			tax_category.description = d.get("description")
+			tax_category.product_tax_code = d.get("product_tax_code")
+			tax_category.category_name = d.get("name")
+			tax_category.db_insert()
+
+def make_custom_fields(update=True):
+	custom_fields = {
+		'Sales Invoice Item': [
+			dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
+				label='Product Tax Category', fetch_from='item_code.product_tax_category'),
+			dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
+				label='Tax Collectable', read_only=1),
+			dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
+				label='Taxable Amount', read_only=1)
+		],
+		'Item': [
+			dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
+				label='Product Tax Category')
+		]
+	}
+	create_custom_fields(custom_fields, update=update)
+
+def add_permissions():
+	doctype = "Product Tax Category"
+	for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
+		add_permission(doctype, role, 0)
+		update_permission_property(doctype, role, 0, 'write', 1)
+		update_permission_property(doctype, role, 0, 'create', 1)
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index 870a4ef..2a7243c 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -4,7 +4,7 @@
 import taxjar
 from frappe import _
 from frappe.contacts.doctype.address.address import get_company_address
-from frappe.utils import cint
+from frappe.utils import cint, flt
 
 from erpnext import get_default_company
 
@@ -103,7 +103,7 @@
 
 	shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
 
-	line_items = [get_line_item_dict(item) for item in doc.items]
+	line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
 
 	if from_shipping_state not in SUPPORTED_STATE_CODES:
 		from_shipping_state = get_state_code(from_address, 'Company')
@@ -139,14 +139,21 @@
 
 	return state_code
 
-def get_line_item_dict(item):
-	return dict(
+def get_line_item_dict(item, docstatus):
+	tax_dict = dict(
 		id = item.get('idx'),
 		quantity = item.get('qty'),
 		unit_price = item.get('rate'),
 		product_tax_code = item.get('product_tax_category')
 	)
 
+	if docstatus == 1:
+		tax_dict.update({
+			'sales_tax':item.get('tax_collectable')
+		})
+
+	return tax_dict
+
 def set_sales_tax(doc, method):
 	if not TAXJAR_CALCULATE_TAX:
 		return
@@ -164,6 +171,9 @@
 		setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
 		return
 
+	# check if delivering within a nexus
+	check_for_nexus(doc, tax_dict)
+
 	tax_data = validate_tax_request(tax_dict)
 	if tax_data is not None:
 		if not tax_data.amount_to_collect:
@@ -191,6 +201,17 @@
 
 			doc.run_method("calculate_taxes_and_totals")
 
+def check_for_nexus(doc, tax_dict):
+	if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
+		for item in doc.get("items"):
+			item.tax_collectable = flt(0)
+			item.taxable_amount = flt(0)
+
+		for tax in doc.taxes:
+			if tax.account_head == TAX_ACCOUNT_HEAD:
+				doc.taxes.remove(tax)
+		return
+
 def check_sales_tax_exemption(doc):
 	# if the party is exempt from sales tax, then set all tax account heads to zero
 	sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f15c65e..86480be 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -286,6 +286,8 @@
 erpnext.patches.v13_0.migrate_stripe_api
 erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
 erpnext.patches.v13_0.einvoicing_deprecation_warning
+execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
+execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
 erpnext.patches.v13_0.custom_fields_for_taxjar_integration
 erpnext.patches.v14_0.delete_einvoicing_doctypes
 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
diff --git a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
index eee9f11..e136d64 100644
--- a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
+++ b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
@@ -3,7 +3,7 @@
 import frappe
 from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
 
-from erpnext.regional.united_states.setup import add_permissions
+from erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings import add_permissions
 
 
 def execute():
@@ -11,7 +11,12 @@
 	if not company:
 		return
 
-	frappe.reload_doc("regional", "doctype", "product_tax_category")
+	TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+	TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+	TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+
+	if (not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE):
+		return
 
 	custom_fields = {
 		'Sales Invoice Item': [
@@ -29,4 +34,4 @@
 	}
 	create_custom_fields(custom_fields, update=True)
 	add_permissions()
-	frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=True)
+	frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True)
diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py
index 9c183af..cf78f92 100644
--- a/erpnext/regional/united_states/setup.py
+++ b/erpnext/regional/united_states/setup.py
@@ -14,30 +14,9 @@
 		setup_company_independent_fixtures(patch=patch)
 
 def setup_company_independent_fixtures(company=None, patch=True):
-	add_product_tax_categories()
 	make_custom_fields()
-	add_permissions()
-	frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
 	add_print_formats()
 
-# Product Tax categories imported from taxjar api
-def add_product_tax_categories():
-	with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
-		tax_categories = json.loads(f.read())
-	create_tax_categories(tax_categories['categories'])
-
-def create_tax_categories(data):
-	for d in data:
-		tax_category = frappe.new_doc('Product Tax Category')
-		tax_category.description = d.get("description")
-		tax_category.product_tax_code = d.get("product_tax_code")
-		tax_category.category_name = d.get("name")
-		try:
-			tax_category.db_insert()
-		except frappe.DuplicateEntryError:
-			pass
-
-
 def make_custom_fields(update=True):
 	custom_fields = {
 		'Supplier': [
@@ -59,29 +38,10 @@
 		'Quotation': [
 			dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges',
 				label='Is customer exempted from sales tax?')
-		],
-		'Sales Invoice Item': [
-			dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
-				label='Product Tax Category', fetch_from='item_code.product_tax_category'),
-			dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
-				label='Tax Collectable', read_only=1),
-			dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
-				label='Taxable Amount', read_only=1)
-		],
-		'Item': [
-			dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
-				label='Product Tax Category')
 		]
 	}
 	create_custom_fields(custom_fields, update=update)
 
-def add_permissions():
-	doctype = "Product Tax Category"
-	for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
-		add_permission(doctype, role, 0)
-		update_permission_property(doctype, role, 0, 'write', 1)
-		update_permission_property(doctype, role, 0, 'create', 1)
-
 def add_print_formats():
 	frappe.reload_doc("regional", "print_format", "irs_1099_form")
 	frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)