[Agriculture] Sort crop tasks chronologically and optimize disease task creation (#14367)

* Improve max crop period check and sort agriculture tasks chronologically

* Reformat crop cycle logic and add field to optimize task creation
diff --git a/erpnext/agriculture/doctype/crop/crop.py b/erpnext/agriculture/doctype/crop/crop.py
index 52594f4..ef02613 100644
--- a/erpnext/agriculture/doctype/crop/crop.py
+++ b/erpnext/agriculture/doctype/crop/crop.py
@@ -3,23 +3,31 @@
 # For license information, please see license.txt
 
 from __future__ import unicode_literals
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe import _
+
 
 class Crop(Document):
 	def validate(self):
-		max_period = 0
+		self.validate_crop_tasks()
+
+	def validate_crop_tasks(self):
 		for task in self.agriculture_task:
-			# validate start_day is not > end_day
 			if task.start_day > task.end_day:
 				frappe.throw(_("Start day is greater than end day in task '{0}'").format(task.task_name))
-			# to calculate the period of the Crop Cycle
-			if task.end_day > max_period: max_period = task.end_day
-		if max_period > self.period: self.period = max_period
+
+		# Verify that the crop period is correct
+		max_crop_period = max([task.end_day for task in self.agriculture_task])
+		self.period = max(self.period, max_crop_period)
+
+		# Sort the crop tasks based on start days,
+		# maintaining the order for same-day tasks
+		self.agriculture_task.sort(key=lambda task: task.start_day)
+
 
 @frappe.whitelist()
 def get_item_details(item_code):
 	item = frappe.get_doc('Item', item_code)
-	return { "uom": item.stock_uom, "rate": item.valuation_rate }
\ No newline at end of file
+	return {"uom": item.stock_uom, "rate": item.valuation_rate}
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
index 1d9f324..e090706 100644
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
@@ -3,43 +3,48 @@
 # For license information, please see license.txt
 
 from __future__ import unicode_literals
+
+import ast
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
-import ast
+from frappe.utils import add_days
+
 
 class CropCycle(Document):
 	def validate(self):
-		if self.is_new():
-			crop = frappe.get_doc('Crop', self.crop)
-			self.create_project(crop.period, crop.agriculture_task)
-			if not self.crop_spacing_uom:
-				self.crop_spacing_uom = crop.crop_spacing_uom
-			if not self.row_spacing_uom:
-				self.row_spacing_uom = crop.row_spacing_uom
-			if not self.project:
-				self.project = self.name
-			disease = []
-			for detected_disease in self.detected_disease:
-				disease.append(detected_disease.name)
-			if disease != []:
-				self.update_disease(disease)
-		else:
-			old_disease, new_disease = [], []
-			for detected_disease in self.detected_disease:
-				new_disease.append(detected_disease.name)
-			for detected_disease in self.get_doc_before_save().get('detected_disease'):
-				old_disease.append(detected_disease.name)
-			if list(set(new_disease)-set(old_disease)) != []:
-				self.update_disease(list(set(new_disease)-set(old_disease)))
-				frappe.msgprint(_("All tasks for the detected diseases were imported"))
+		self.set_missing_values()
 
-	def update_disease(self, disease_hashes):
-		new_disease = []
+	def after_insert(self):
+		self.create_crop_cycle_project()
+		self.create_tasks_for_diseases()
+
+	def on_update(self):
+		self.create_tasks_for_diseases()
+
+	def set_missing_values(self):
+		crop = frappe.get_doc('Crop', self.crop)
+
+		if not self.crop_spacing_uom:
+			self.crop_spacing_uom = crop.crop_spacing_uom
+
+		if not self.row_spacing_uom:
+			self.row_spacing_uom = crop.row_spacing_uom
+
+	def create_crop_cycle_project(self):
+		crop = frappe.get_doc('Crop', self.crop)
+
+		self.project = self.create_project(crop.period, crop.agriculture_task)
+		self.create_task(crop.agriculture_task, self.project, self.start_date)
+
+	def create_tasks_for_diseases(self):
 		for disease in self.detected_disease:
-			for disease_hash in disease_hashes:
-				if disease.name == disease_hash:
-					self.import_disease_tasks(disease.disease, disease.start_date)
+			if not disease.tasks_created:
+				self.import_disease_tasks(disease.disease, disease.start_date)
+				disease.tasks_created = True
+
+				frappe.msgprint(_("Tasks have been created for managing the {0} disease (on row {1})".format(disease.disease, disease.idx)))
 
 	def import_disease_tasks(self, disease, start_date):
 		disease_doc = frappe.get_doc('Disease', disease)
@@ -47,58 +52,77 @@
 
 	def create_project(self, period, crop_tasks):
 		project = frappe.new_doc("Project")
-		project.project_name = self.title
-		project.expected_start_date = self.start_date
-		project.expected_end_date = frappe.utils.data.add_days(self.start_date, period-1)
+		project.update({
+			"project_name": self.title,
+			"expected_start_date": self.start_date,
+			"expected_end_date": add_days(self.start_date, period - 1)
+		})
 		project.insert()
-		self.create_task(crop_tasks, project.as_dict.im_self.name, self.start_date)
-		return project.as_dict.im_self.name
+
+		return project.name
 
 	def create_task(self, crop_tasks, project_name, start_date):
 		for crop_task in crop_tasks:
 			task = frappe.new_doc("Task")
-			task.subject = crop_task.get("task_name")
-			task.priority = crop_task.get("priority")
-			task.project = project_name
-			task.exp_start_date = frappe.utils.data.add_days(start_date, crop_task.get("start_day")-1)
-			task.exp_end_date = frappe.utils.data.add_days(start_date, crop_task.get("end_day")-1)
+			task.update({
+				"subject": crop_task.get("task_name"),
+				"priority": crop_task.get("priority"),
+				"project": project_name,
+				"exp_start_date": add_days(start_date, crop_task.get("start_day") - 1),
+				"exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
+			})
 			task.insert()
 
 	def reload_linked_analysis(self):
 		linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
 		required_fields = ['location', 'name', 'collection_datetime']
 		output = {}
+
 		for doctype in linked_doctypes:
 			output[doctype] = frappe.get_all(doctype, fields=required_fields)
+
 		output['Land Unit'] = []
+
 		for land in self.linked_land_unit:
 			output['Land Unit'].append(frappe.get_doc('Land Unit', land.land_unit))
 
-		frappe.publish_realtime("List of Linked Docs", output, user=frappe.session.user)
+		frappe.publish_realtime("List of Linked Docs",
+								output, user=frappe.session.user)
 
 	def append_to_child(self, obj_to_append):
 		for doctype in obj_to_append:
 			for doc_name in set(obj_to_append[doctype]):
 				self.append(doctype, {doctype: doc_name})
+
 		self.save()
 
-	def get_coordinates(self, doc):
-		return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('coordinates')
 
-	def get_geometry_type(self, doc):
-		return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('type')
+def get_coordinates(doc):
+	return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('coordinates')
 
-	def is_in_land_unit(self, point, vs):
-		x, y = point
-		inside = False
-		j = len(vs)-1
-		i = 0
-		while i < len(vs):
-			xi, yi = vs[i]
-			xj, yj = vs[j]
-			intersect = ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
-			if intersect:
-				inside = not inside
-			i = j
-			j += 1
-		return inside
\ No newline at end of file
+
+def get_geometry_type(doc):
+	return ast.literal_eval(doc.location).get('features')[0].get('geometry').get('type')
+
+
+def is_in_land_unit(point, vs):
+	x, y = point
+	inside = False
+
+	j = len(vs) - 1
+	i = 0
+
+	while i < len(vs):
+		xi, yi = vs[i]
+		xj, yj = vs[j]
+
+		intersect = ((yi > y) != (yj > y)) and (
+			x < (xj - xi) * (y - yi) / (yj - yi) + xi)
+
+		if intersect:
+			inside = not inside
+
+		i = j
+		j += 1
+
+	return inside
diff --git a/erpnext/agriculture/doctype/detected_disease/detected_disease.json b/erpnext/agriculture/doctype/detected_disease/detected_disease.json
index cf44149..bfed9a7 100644
--- a/erpnext/agriculture/doctype/detected_disease/detected_disease.json
+++ b/erpnext/agriculture/doctype/detected_disease/detected_disease.json
@@ -14,6 +14,7 @@
  "fields": [
   {
    "allow_bulk_edit": 0, 
+   "allow_in_quick_entry": 0, 
    "allow_on_submit": 0, 
    "bold": 0, 
    "collapsible": 0, 
@@ -41,10 +42,12 @@
    "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, 
@@ -71,6 +74,40 @@
    "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": "tasks_created", 
+   "fieldtype": "Check", 
+   "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": "Tasks Created", 
+   "length": 0, 
+   "no_copy": 1, 
+   "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": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "translatable": 0, 
    "unique": 0
   }
  ], 
@@ -84,7 +121,7 @@
  "issingle": 0, 
  "istable": 1, 
  "max_attachments": 0, 
- "modified": "2017-11-26 21:10:14.753511", 
+ "modified": "2018-06-06 02:24:52.131482", 
  "modified_by": "Administrator", 
  "module": "Agriculture", 
  "name": "Detected Disease",