Merge branch 'develop' into field-filters-e-com
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 532485f..5a46002 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,6 +12,7 @@
         uses: actions/checkout@v2
         with:
           fetch-depth: 0
+          persist-credentials: false
       - name: Setup Node.js v14
         uses: actions/setup-node@v2
         with:
@@ -21,5 +22,10 @@
           npm install @semantic-release/git @semantic-release/exec --no-save
       - name: Create Release
         env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+          GIT_AUTHOR_NAME: "Frappe PR Bot"
+          GIT_AUTHOR_EMAIL: "developers@frappe.io"
+          GIT_COMMITTER_NAME: "Frappe PR Bot"
+          GIT_COMMITTER_EMAIL: "developers@frappe.io"
         run: npx semantic-release
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
index 81b44f8..ced42bb 100644
--- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
@@ -153,6 +153,31 @@
 		log = make_checkin(employee, timestamp)
 		self.assertIsNone(log.shift)
 
+	def test_fetch_shift_for_assignment_with_end_date(self):
+		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
+
+		# shift setup for 8-12
+		shift1 = setup_shift_type()
+		# 12:30 - 16:30
+		shift2 = setup_shift_type(shift_type="Shift 2", start_time="12:30:00", end_time="16:30:00")
+
+		date = getdate()
+		make_shift_assignment(shift1.name, employee, date, add_days(date, 15))
+		make_shift_assignment(shift2.name, employee, date, add_days(date, 15))
+
+		timestamp = datetime.combine(date, get_time("08:45:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift1.name)
+
+		timestamp = datetime.combine(date, get_time("12:45:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertEqual(log.shift, shift2.name)
+
+		# log after end date
+		timestamp = datetime.combine(add_days(date, 16), get_time("12:45:00"))
+		log = make_checkin(employee, timestamp)
+		self.assertIsNone(log.shift)
+
 	def test_shift_start_and_end_timings(self):
 		employee = make_employee("test_employee_checkin@example.com", company="_Test Company")
 
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 0b21c00..51298de 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -121,7 +121,7 @@
 
 @frappe.whitelist()
 def get_events(start, end, filters=None):
-	events = []
+	from frappe.desk.calendar import get_event_conditions
 
 	employee = frappe.db.get_value(
 		"Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True
@@ -132,20 +132,22 @@
 		employee = ""
 		company = frappe.db.get_value("Global Defaults", None, "default_company")
 
-	from frappe.desk.reportview import get_filters_cond
-
-	conditions = get_filters_cond("Shift Assignment", filters, [])
-	add_assignments(events, start, end, conditions=conditions)
+	conditions = get_event_conditions("Shift Assignment", filters)
+	events = add_assignments(start, end, conditions=conditions)
 	return events
 
 
-def add_assignments(events, start, end, conditions=None):
+def add_assignments(start, end, conditions=None):
+	events = []
+
 	query = """select name, start_date, end_date, employee_name,
 		employee, docstatus, shift_type
 		from `tabShift Assignment` where
-		start_date >= %(start_date)s
-		or end_date <=  %(end_date)s
-		or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
+		(
+			start_date >= %(start_date)s
+			or end_date <=  %(end_date)s
+			or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
+		)
 		and docstatus = 1"""
 	if conditions:
 		query += conditions
@@ -251,7 +253,7 @@
 				Criterion.any(
 					[
 						assignment.end_date.isnull(),
-						(assignment.end_date.isnotnull() & (getdate(for_timestamp.date()) >= assignment.end_date)),
+						(assignment.end_date.isnotnull() & (getdate(for_timestamp.date()) <= assignment.end_date)),
 					]
 				)
 			)
diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
index 0fe9108..de82a24 100644
--- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py
@@ -8,7 +8,7 @@
 from frappe.utils import add_days, getdate, nowdate
 
 from erpnext.hr.doctype.employee.test_employee import make_employee
-from erpnext.hr.doctype.shift_assignment.shift_assignment import OverlappingShiftError
+from erpnext.hr.doctype.shift_assignment.shift_assignment import OverlappingShiftError, get_events
 from erpnext.hr.doctype.shift_type.test_shift_type import make_shift_assignment, setup_shift_type
 
 test_dependencies = ["Shift Type"]
@@ -154,3 +154,18 @@
 		shift_type = setup_shift_type(shift_type="Shift 2", start_time="13:00:00", end_time="15:00:00")
 		date = getdate()
 		make_shift_assignment(shift_type.name, employee, date)
+
+	def test_shift_assignment_calendar(self):
+		employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company")
+		employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company")
+
+		shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
+		date = getdate()
+		shift1 = make_shift_assignment(shift_type.name, employee1, date)
+		make_shift_assignment(shift_type.name, employee2, date)
+
+		events = get_events(
+			start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]]
+		)
+		self.assertEqual(len(events), 1)
+		self.assertEqual(events[0]["name"], shift1.name)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index b2824e1..b6646b1 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -73,10 +73,22 @@
 		if (frm.doc.docstatus == 0 && !frm.is_new() &&
 			(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
 			&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
-			frm.trigger("prepare_timer_buttons");
+
+			// if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
+			// and if stock mvt for WIP is required
+			if (frm.doc.work_order) {
+				frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
+					if (result.skip_transfer === 1 || result.status == 'In Process') {
+						frm.trigger("prepare_timer_buttons");
+					}
+				});
+			} else {
+				frm.trigger("prepare_timer_buttons");
+			}
 		}
 
 		frm.trigger("setup_quality_inspection");
+
 		if (frm.doc.work_order) {
 			frappe.db.get_value('Work Order', frm.doc.work_order,
 				'transfer_material_against').then((r) => {
diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py
index edd0a97..45acf49 100644
--- a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py
+++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py
@@ -10,54 +10,58 @@
 
 	frappe.reload_doc("hr", "doctype", "Leave Encashment")
 
-	additional_salaries = frappe.get_all(
-		"Additional Salary",
-		fields=["name", "salary_slip", "type", "salary_component"],
-		filters={"salary_slip": ["!=", ""]},
-		group_by="salary_slip",
-	)
-	leave_encashments = frappe.get_all(
-		"Leave Encashment",
-		fields=["name", "additional_salary"],
-		filters={"additional_salary": ["!=", ""]},
-	)
-	employee_incentives = frappe.get_all(
-		"Employee Incentive",
-		fields=["name", "additional_salary"],
-		filters={"additional_salary": ["!=", ""]},
-	)
-
-	for incentive in employee_incentives:
-		frappe.db.sql(
-			""" UPDATE `tabAdditional Salary`
-			SET ref_doctype = 'Employee Incentive', ref_docname = %s
-			WHERE name = %s
-		""",
-			(incentive["name"], incentive["additional_salary"]),
+	if frappe.db.has_column("Leave Encashment", "additional_salary"):
+		leave_encashments = frappe.get_all(
+			"Leave Encashment",
+			fields=["name", "additional_salary"],
+			filters={"additional_salary": ["!=", ""]},
 		)
-
-	for leave_encashment in leave_encashments:
-		frappe.db.sql(
-			""" UPDATE `tabAdditional Salary`
-			SET ref_doctype = 'Leave Encashment', ref_docname = %s
-			WHERE name = %s
-		""",
-			(leave_encashment["name"], leave_encashment["additional_salary"]),
-		)
-
-	salary_slips = [sal["salary_slip"] for sal in additional_salaries]
-
-	for salary in additional_salaries:
-		comp_type = "earnings" if salary["type"] == "Earning" else "deductions"
-		if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1:
+		for leave_encashment in leave_encashments:
 			frappe.db.sql(
-				"""
-				UPDATE `tabSalary Detail`
-				SET additional_salary = %s
-				WHERE parenttype = 'Salary Slip'
-					and parentfield = %s
-					and parent = %s
-					and salary_component = %s
+				""" UPDATE `tabAdditional Salary`
+				SET ref_doctype = 'Leave Encashment', ref_docname = %s
+				WHERE name = %s
 			""",
-				(salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]),
+				(leave_encashment["name"], leave_encashment["additional_salary"]),
 			)
+
+	if frappe.db.has_column("Employee Incentive", "additional_salary"):
+		employee_incentives = frappe.get_all(
+			"Employee Incentive",
+			fields=["name", "additional_salary"],
+			filters={"additional_salary": ["!=", ""]},
+		)
+
+		for incentive in employee_incentives:
+			frappe.db.sql(
+				""" UPDATE `tabAdditional Salary`
+				SET ref_doctype = 'Employee Incentive', ref_docname = %s
+				WHERE name = %s
+			""",
+				(incentive["name"], incentive["additional_salary"]),
+			)
+
+	if frappe.db.has_column("Additional Salary", "salary_slip"):
+		additional_salaries = frappe.get_all(
+			"Additional Salary",
+			fields=["name", "salary_slip", "type", "salary_component"],
+			filters={"salary_slip": ["!=", ""]},
+			group_by="salary_slip",
+		)
+
+		salary_slips = [sal["salary_slip"] for sal in additional_salaries]
+
+		for salary in additional_salaries:
+			comp_type = "earnings" if salary["type"] == "Earning" else "deductions"
+			if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1:
+				frappe.db.sql(
+					"""
+					UPDATE `tabSalary Detail`
+					SET additional_salary = %s
+					WHERE parenttype = 'Salary Slip'
+						and parentfield = %s
+						and parent = %s
+						and salary_component = %s
+				""",
+					(salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]),
+				)
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index f317569..a97ad79 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -1074,7 +1074,7 @@
 				"Distance": cint(eway_bill_details.distance),
 				"TransMode": eway_bill_details.mode_of_transport,
 				"TransId": eway_bill_details.gstin,
-				"TransName": eway_bill_details.transporter,
+				"TransName": eway_bill_details.name,
 				"TrnDocDt": eway_bill_details.document_date,
 				"TrnDocNo": eway_bill_details.document_name,
 				"VehNo": eway_bill_details.vehicle_no,
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index a410513..dcfb10a 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -81,7 +81,7 @@
 			ON sii.so_detail = soi.name and sii.docstatus = 1)
 		LEFT JOIN `tabDelivery Note Item` dni
 			on dni.so_detail = soi.name
-		RIGHT JOIN `tabDelivery Note` dn
+		LEFT JOIN `tabDelivery Note` dn
 			on dni.parent = dn.name and dn.docstatus = 1
 		WHERE
 			soi.parent = so.name
diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
new file mode 100644
index 0000000..25cbb73
--- /dev/null
+++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
@@ -0,0 +1,166 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days
+
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.selling.report.sales_order_analysis.sales_order_analysis import execute
+from erpnext.stock.doctype.item.test_item import create_item
+
+test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"]
+
+
+class TestSalesOrderAnalysis(FrappeTestCase):
+	def create_sales_order(self, transaction_date):
+		item = create_item(item_code="_Test Excavator", is_stock_item=0)
+		so = make_sales_order(
+			transaction_date=transaction_date,
+			item=item.item_code,
+			qty=10,
+			rate=100000,
+			do_not_save=True,
+		)
+		so.po_no = ""
+		so.taxes_and_charges = ""
+		so.taxes = ""
+		so.items[0].delivery_date = add_days(transaction_date, 15)
+		so.save()
+		so.submit()
+		return item, so
+
+	def create_sales_invoice(self, so):
+		sinv = make_sales_invoice(so.name)
+		sinv.posting_date = so.transaction_date
+		sinv.taxes_and_charges = ""
+		sinv.taxes = ""
+		sinv.insert()
+		sinv.submit()
+		return sinv
+
+	def create_delivery_note(self, so):
+		dn = make_delivery_note(so.name)
+		dn.set_posting_time = True
+		dn.posting_date = add_days(so.transaction_date, 1)
+		dn.save()
+		dn.submit()
+		return dn
+
+	def test_01_so_to_deliver_and_bill(self):
+		transaction_date = "2021-06-01"
+		item, so = self.create_sales_order(transaction_date)
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+				"status": ["To Deliver and Bill"],
+			}
+		)
+		expected_value = {
+			"status": "To Deliver and Bill",
+			"sales_order": so.name,
+			"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+			"qty": 10,
+			"delivered_qty": 0,
+			"pending_qty": 10,
+			"qty_to_bill": 10,
+			"time_taken_to_deliver": 0,
+		}
+		self.assertEqual(len(data), 1)
+		for key, val in expected_value.items():
+			with self.subTest(key=key, val=val):
+				self.assertEqual(data[0][key], val)
+
+	def test_02_so_to_deliver(self):
+		transaction_date = "2021-06-01"
+		item, so = self.create_sales_order(transaction_date)
+		self.create_sales_invoice(so)
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+				"status": ["To Deliver"],
+			}
+		)
+		expected_value = {
+			"status": "To Deliver",
+			"sales_order": so.name,
+			"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+			"qty": 10,
+			"delivered_qty": 0,
+			"pending_qty": 10,
+			"qty_to_bill": 0,
+			"time_taken_to_deliver": 0,
+		}
+		self.assertEqual(len(data), 1)
+		for key, val in expected_value.items():
+			with self.subTest(key=key, val=val):
+				self.assertEqual(data[0][key], val)
+
+	def test_03_so_to_bill(self):
+		transaction_date = "2021-06-01"
+		item, so = self.create_sales_order(transaction_date)
+		self.create_delivery_note(so)
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+				"status": ["To Bill"],
+			}
+		)
+		expected_value = {
+			"status": "To Bill",
+			"sales_order": so.name,
+			"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+			"qty": 10,
+			"delivered_qty": 10,
+			"pending_qty": 0,
+			"qty_to_bill": 10,
+			"time_taken_to_deliver": 86400,
+		}
+		self.assertEqual(len(data), 1)
+		for key, val in expected_value.items():
+			with self.subTest(key=key, val=val):
+				self.assertEqual(data[0][key], val)
+
+	def test_04_so_completed(self):
+		transaction_date = "2021-06-01"
+		item, so = self.create_sales_order(transaction_date)
+		self.create_sales_invoice(so)
+		self.create_delivery_note(so)
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+				"status": ["Completed"],
+			}
+		)
+		expected_value = {
+			"status": "Completed",
+			"sales_order": so.name,
+			"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+			"qty": 10,
+			"delivered_qty": 10,
+			"pending_qty": 0,
+			"qty_to_bill": 0,
+			"billed_qty": 10,
+			"time_taken_to_deliver": 86400,
+		}
+		self.assertEqual(len(data), 1)
+		for key, val in expected_value.items():
+			with self.subTest(key=key, val=val):
+				self.assertEqual(data[0][key], val)
+
+	def test_05_all_so_status(self):
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+			}
+		)
+		# SO's from first 4 test cases should be in output
+		self.assertEqual(len(data), 4)