Merge branch 'putaway' of https://github.com/marination/erpnext into putaway
diff --git a/README.md b/README.md
index 0f6a521..15782a2 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
         <p>ERP made simple</p>
     </p>
 
-[![Build Status](https://travis-ci.com/frappe/erpnext.svg)](https://travis-ci.com/frappe/erpnext)
+[![Build Status](https://api.travis-ci.com/frappe/erpnext.svg?branch=develop)](https://travis-ci.com/frappe/erpnext)
 [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
 [![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)
 
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index a0b0cbb..ef77674 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -7,6 +7,7 @@
 import unittest
 from frappe.utils import today
 from erpnext.accounts.utils import get_fiscal_year
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
 
 test_dependencies = ["Supplier Group"]
 
@@ -103,17 +104,20 @@
 
 	def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
 		invoices = []
-		frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
-		pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+		doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
+			tax_withholding_category="Single Threshold TDS")
+		supplier = doc.name
+
+		pi = create_purchase_invoice(supplier=supplier)
 		pi.submit()
 		invoices.append(pi)
 
 		# TDS not applied
-		pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
+		pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
 		pi.submit()
 		invoices.append(pi)
 
-		pi = create_purchase_invoice(supplier="Test TDS Supplier2")
+		pi = create_purchase_invoice(supplier=supplier)
 		pi.submit()
 		invoices.append(pi)
 
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index a377ec9..f9c8d35 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -120,3 +120,20 @@
 
         # Rollback
         address.delete()
+
+def create_supplier(**args):
+    args = frappe._dict(args)
+
+    try:
+        doc = frappe.get_doc({
+            "doctype": "Supplier",
+            "supplier_name": args.supplier_name,
+            "supplier_group": args.supplier_group or "Services",
+            "supplier_type": args.supplier_type or "Company",
+            "tax_withholding_category": args.tax_withholding_category
+        }).insert()
+
+        return doc
+
+    except frappe.DuplicateEntryError:
+        return frappe.get_doc("Supplier", args.supplier_name)
\ No newline at end of file
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index f743d70..2d2fff8 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -229,9 +229,9 @@
 
 	def check_expense_account(self, item):
 		if not item.get("expense_account"):
-			frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \
-				Account in the Items table").format(item.idx, frappe.bold(item.item_code)),
-				title=_("Expense Account Missing"))
+			msg = _("Please set an Expense Account in the Items table")
+			frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}")
+				.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
 
 		else:
 			is_expense_account = frappe.db.get_value("Account",
@@ -247,7 +247,9 @@
 		for d in self.items:
 			if not d.batch_no: continue
 
-			serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})]
+			serial_nos = [sr.name for sr in frappe.get_all("Serial No",
+				{'batch_no': d.batch_no, 'status': 'Inactive'})]
+
 			if serial_nos:
 				frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
 
diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py
index 8aa7453..efbaa71 100644
--- a/erpnext/erpnext_integrations/connectors/shopify_connection.py
+++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py
@@ -149,26 +149,28 @@
 		si.shopify_order_number = shopify_order.get("name")
 		si.set_posting_time = 1
 		si.posting_date = posting_date
+		si.due_date = posting_date
 		si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-"
 		si.flags.ignore_mandatory = True
 		set_cost_center(si.items, shopify_settings.cost_center)
 		si.insert(ignore_mandatory=True)
 		si.submit()
-		make_payament_entry_against_sales_invoice(si, shopify_settings)
+		make_payament_entry_against_sales_invoice(si, shopify_settings, posting_date)
 		frappe.db.commit()
 
 def set_cost_center(items, cost_center):
 	for item in items:
 		item.cost_center = cost_center
 
-def make_payament_entry_against_sales_invoice(doc, shopify_settings):
+def make_payament_entry_against_sales_invoice(doc, shopify_settings, posting_date=None):
 	from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
-	payemnt_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
-	payemnt_entry.flags.ignore_mandatory = True
-	payemnt_entry.reference_no = doc.name
-	payemnt_entry.reference_date = nowdate()
-	payemnt_entry.insert(ignore_permissions=True)
-	payemnt_entry.submit()
+	payment_entry = get_payment_entry(doc.doctype, doc.name, bank_account=shopify_settings.cash_bank_account)
+	payment_entry.flags.ignore_mandatory = True
+	payment_entry.reference_no = doc.name
+	payment_entry.posting_date = posting_date or nowdate()
+	payment_entry.reference_date = posting_date or nowdate()
+	payment_entry.insert(ignore_permissions=True)
+	payment_entry.submit()
 
 def create_delivery_note(shopify_order, shopify_settings, so):
 	if not cint(shopify_settings.sync_delivery_note):
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index b4c57d7..741176f 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -237,6 +237,9 @@
 	"Website Settings": {
 		"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
 	},
+	"Tax Category": {
+		"validate": "erpnext.regional.india.utils.validate_tax_category"
+	},
 	"Sales Invoice": {
 		"on_submit": [
 			"erpnext.regional.create_transaction_log",
diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py
index 9b2de0e..d337959 100644
--- a/erpnext/hr/doctype/department_approver/department_approver.py
+++ b/erpnext/hr/doctype/department_approver/department_approver.py
@@ -20,7 +20,7 @@
 	approvers = []
 	department_details = {}
 	department_list = []
-	employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
+	employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True)
 
 	employee_department = filters.get("department") or employee.department
 	if employee_department:
@@ -59,11 +59,9 @@
 				and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
 
 	if len(approvers) == 0:
-		frappe.throw(_("Please set {0} for the Employee or for Department: {1}").
-			format(
-				field_name, frappe.bold(employee_department),
-				frappe.bold(employee.name)
-			),
-			title=_(field_name + " Missing"))
+		error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name))
+		if department_list:
+			error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
+		frappe.throw(error_msg, title=_(field_name + " Missing"))
 
 	return set(tuple(approver) for approver in approvers)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 4dfa78b..d15d81e 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -353,17 +353,19 @@
 
 @frappe.whitelist()
 def get_operations(doctype, txt, searchfield, start, page_len, filters):
-	if filters.get("work_order"):
-		args = {"parent": filters.get("work_order")}
-		if txt:
-			args["operation"] = ("like", "%{0}%".format(txt))
+	if not filters.get("work_order"):
+		frappe.msgprint(_("Please select a Work Order first."))
+		return []
+	args = {"parent": filters.get("work_order")}
+	if txt:
+		args["operation"] = ("like", "%{0}%".format(txt))
 
-		return frappe.get_all("Work Order Operation",
-			filters = args,
-			fields = ["distinct operation as operation"],
-			limit_start = start,
-			limit_page_length = page_len,
-			order_by="idx asc", as_list=1)
+	return frappe.get_all("Work Order Operation",
+		filters = args,
+		fields = ["distinct operation as operation"],
+		limit_start = start,
+		limit_page_length = page_len,
+		order_by="idx asc", as_list=1)
 
 @frappe.whitelist()
 def make_material_request(source_name, target_doc=None):
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 2ac6fa0..84f5c34 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -25,11 +25,11 @@
 	],
 	"formatter": function(value, row, column, data, default_formatter) {
 		value = default_formatter(value, row, column, data);
-		if (column.id == "Item"){
-			if (data["Enough Parts to Build"] > 0){
-				value = `<a style='color:green' href="#Form/Item/${data['Item']}" data-doctype="Item">${data['Item']}</a>`
+		if (column.id == "item") {
+			if (data["Enough Parts to Build"] > 0) {
+				value = `<a style='color:green' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
 			} else {
-				value = `<a style='color:red' href="#Form/Item/${data['Item']}" data-doctype="Item">${data['Item']}</a>`
+				value = `<a style='color:red' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
 			}
 		}
 		return value
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 3b6a28f..3c15647 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -31,12 +31,12 @@
 				args: {
 					party_details: JSON.stringify(party_details),
 					doctype: frm.doc.doctype,
-					company: frm.doc.company,
-					return_taxes: 1
+					company: frm.doc.company
 				},
 				callback: function(r) {
 					if(r.message) {
 						frm.set_value('taxes_and_charges', r.message.taxes_and_charges);
+						frm.set_value('place_of_supply', r.message.place_of_supply);
 					} else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) {
 						frm.set_value('taxes_and_charges', '');
 						frm.set_value('taxes', []);
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 5458001..62487ba 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -51,6 +51,13 @@
 			frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
 				.format(doc.gst_state_number))
 
+def validate_tax_category(doc, method):
+	if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
+		if doc.is_inter_state:
+			frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
+		else:
+			frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state))
+
 def update_gst_category(doc, method):
 	for link in doc.links:
 		if link.link_doctype in ['Customer', 'Supplier']:
@@ -85,8 +92,7 @@
 		total += digit
 		factor = 2 if factor == 1 else 1
 	if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]:
-		frappe.throw(_("""Invalid {0}! The check digit validation has failed.
-			Please ensure you've typed the {0} correctly.""".format(label)))
+		frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
 
 def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
 	if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
@@ -150,7 +156,7 @@
 			return cstr(address.gst_state_number) + "-" + cstr(address.gst_state)
 
 @frappe.whitelist()
-def get_regional_address_details(party_details, doctype, company, return_taxes=None):
+def get_regional_address_details(party_details, doctype, company):
 	if isinstance(party_details, string_types):
 		party_details = json.loads(party_details)
 		party_details = frappe._dict(party_details)
@@ -160,7 +166,7 @@
 	if is_internal_transfer(party_details, doctype):
 		party_details.taxes_and_charges = ''
 		party_details.taxes = ''
-		return
+		return party_details
 
 	if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
 		master_doctype = "Sales Taxes and Charges Template"
@@ -168,26 +174,26 @@
 		get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
 		get_tax_template_based_on_category(master_doctype, company, party_details)
 
-		if party_details.get('taxes_and_charges') and return_taxes:
+		if party_details.get('taxes_and_charges'):
 			return party_details
 
 		if not party_details.company_gstin:
-			return
+			return party_details
 
 	elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
 		master_doctype = "Purchase Taxes and Charges Template"
 		get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
 		get_tax_template_based_on_category(master_doctype, company, party_details)
 
-		if party_details.get('taxes_and_charges') and return_taxes:
+		if party_details.get('taxes_and_charges'):
 			return party_details
 
 		if not party_details.supplier_gstin:
-			return
+			return party_details
 
-	if not party_details.place_of_supply: return
+	if not party_details.place_of_supply: return party_details
 
-	if not party_details.company_gstin: return
+	if not party_details.company_gstin: return party_details
 
 	if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
 		and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
@@ -197,12 +203,11 @@
 		default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2])
 
 	if not default_tax:
-		return
+		return party_details
 	party_details["taxes_and_charges"] = default_tax
 	party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
 
-	if return_taxes:
-		return party_details
+	return party_details
 
 def is_internal_transfer(party_details, doctype):
 	if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
@@ -516,7 +521,7 @@
 		data.transType = 1
 		data.actualToStateCode = data.toStateCode
 		shipping_address = billing_address
-	
+
 	if doc.gst_category == 'SEZ':
 		data.toStateCode = 99
 
@@ -755,4 +760,4 @@
 					}, account_currency, item=tax)
 				)
 
-	return gl_entries
\ No newline at end of file
+	return gl_entries
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 3b62c38..be845d9 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -977,15 +977,20 @@
 		# For "Is Stock Item", following doctypes is important
 		# because reserved_qty, ordered_qty and requested_qty updated from these doctypes
 		if field == "is_stock_item":
-			linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item"]
+			linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"]
 
 		for doctype in linked_doctypes:
+			filters={"item_code": self.name, "docstatus": 1}
+
+			if doctype == "Product Bundle":
+				filters={"new_item_code": self.name}
+
 			if doctype in ("Purchase Invoice Item", "Sales Invoice Item",):
 				# If Invoice has Stock impact, only then consider it.
 				if self.stock_ledger_created():
 					return True
 
-			elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}):
+			elif frappe.db.get_value(doctype, filters):
 				return True
 
 	def validate_auto_reorder_enabled_in_stock_settings(self):
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index d964669..2cc4679 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -572,7 +572,8 @@
 			"doctype": "Purchase Invoice",
 			"field_map": {
 				"supplier_warehouse":"supplier_warehouse",
-				"is_return": "is_return"
+				"is_return": "is_return",
+				"bill_date": "bill_date"
 			},
 			"validation": {
 				"docstatus": ["=", 1],
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 067659f..a166657 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -217,7 +217,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2020-10-13 10:33:29.147682",
+ "modified": "2020-11-23 15:26:54.225608",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Settings",
@@ -235,5 +235,6 @@
  ],
  "quick_entry": 1,
  "sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "track_changes": 1
 }
\ No newline at end of file